臭名昭著的错误以及如何以ClickHouse为例避免它们

如果您正在编写代码,请准备解决问题​​。它们肯定是,而且应该从各个方面进行期望:从您的代码和编译器,从操作系统和硬件,以及用户有时会抛出“意外”。如果将集群缩放到宇宙规模,则可能会遇到“空格”错误。尤其是涉及互联网流量的数据时。


阿列克谢·米洛维多夫o6CuFl2Q)将以他在开发和支持ClickHouse方面的经验来谈论最荒谬,令人沮丧和绝望的问题。让我们看看如何调试它们以及开发人员从一开始就应该采取什么措施,以减少出现的问题。

臭虫


如果您编写了一些代码,请立即准备解决问题​​。

代码中的错误。他们是必需的。但是,假设您编写了完美的代码,已编译,但是错误将出现在编译器中,并且代码将无法正常工作。我们修复了编译器,所有内容都已编译-运行它。但是(出乎意料)一切都无法正常工作,因为OS内核中也存在错误

如果操作系统中没有错误,则不可避免地,它们将存在于硬件中。即使您编写了可以在完美硬件上完美运行的完美代码,您仍然会遇到问题,例如配置错误。看来您所做的一切都正确,但是有人在配置文件中犯了一个错误,并且一切都无法正常工作。

修复所有错误后,用户将完成此操作,因为他们不断“错误地”使用您的代码。但是问题绝对不在于用户,而在于代码:您编写的东西很难使用

让我们通过一些示例来看看这些错误。

配置错误


数据删除。来自实践的第一种情况。幸运的是,不是我的也不是Yandex,请不要担心。

入门第一。地图缩减群集(例如Hadoop)的体系结构由多个存储数据的数据服务器(数据节点)和一个或多个知道所有数据在服务器上位置的主服务器组成。

数据节点知道主机的地址并连接到它。该向导监视应放置数据的位置和数据,并向数据节点提供不同的命令:“下载数据X,您必须拥有数据Y,并删除数据Z”。可能出什么问题了?

当新的配置文件上载到所有数据节点时,它们错误地从另一个群集连接到主节点,而不是自己连接。主服务器查看有关数据节点的数据,认为数据不正确,应删除。擦除一半的数据后,便注意到了该问题。


最史诗般的错误是那些导致误删除数据的错误。
避免这种情况非常简单。

不要删除数据。例如,将其放在单独的目录中或延迟删除。首先,我们进行转移,以使用户看不到它们,如果他发现几天之内消失了,我们将把它退回。

如果原因未知,请不要删除意外数据。以编程方式限制删除未知数据的开始:意外的,名称不正确的名称,或者名称过多的名称。管理员会注意到服务器没有启动并写了一些消息,并且会理解。

如果程序执行破坏性操作-在网络级别隔离测试和生产(iptables)。例如,删除文件或发送电子邮件是一种破坏性的操作,因为它会“消耗”某人的注意力。给他们设置一个门槛:可以发送一百封信,对于一千封信可以放置一个安全复选框,该复选框在发生严重事故之前设置。

配置。第二个例子已经来自我的实践。

一家不错的公司以某种方式拥有一个奇怪的ClickHouse集群。奇怪的是,副本没有同步。重新启动服务器后,它没有启动,并且出现一条消息,提示所有数据不正确:“有很多意外的数据,我将无法启动。我们必须设置标志force_restore_data并弄清楚。”

没有人能在公司中弄清楚-他们只是设置了标志。同时,一半的数据消失在某处,从而导致图表出现空白。开发人员转向我,我认为正在发生一些有趣的事情,并决定进行调查。几个小时后的早晨,鸟儿开始在窗外唱歌,我意识到自己一无所知。

ClickHouse服务器使用ZooKeeper服务进行协调。 ClickHouse存储数据,ZooKeeper确定哪些服务器应放置哪些数据:存储有关哪个副本应包含哪些数据的元数据。 ZooKeeper还是一个群集-它根据非常严格的一致性很好的分布式共识算法进行复制。

通常,ZooKeeper是3台计算机,有时是5台计算机。在ClickHouse配置中,所有计算机同时显示,与一台随机计算机建立连接,与之交互,然后该服务器复制所有请求。

发生了什么?该公司有三台ZooKeeper服务器。但是它们不是作为三个节点群集,而是作为三个独立的节点 —来自一个节点的三个群集。一台ClickHouse连接到一台服务器并写入数据。副本服务器希望下载此数据,但找不到它们。重新启动时,服务器连接到另一个ZooKeeper:它发现以前使用过的数据是多余的,必须将其推迟到某个地方。他不会删除它们,而是将它们转移到单独的目录中-在ClickHouse中,数据很难删除。

我决定修复ZooKeeper配置。我重命名所有数据,并ATTACH从目录中请求部分数据detached/unexpeted_*

结果,恢复了所有数据,同步了副本,没有丢失,图形是连续的。该公司很满意,很感激,好像他们已经忘记了以前的一切情况如何。

这些是简单的配置错误。更多错误将在代码中。

代码中的错误


我们用C ++编写代码。这意味着我们已经有问题了。
下一个示例是Yandex.Metrica集群(2015)实际生产错误-C ++代码的结果。该错误是有时用户未响应请求而收到错误消息:

  • “校验和不匹配,数据已损坏”-校验和不匹配,数据已损坏-吓人!
  • “ LRUCache变得不一致。里面肯定有一个错误-缓存不一致,很可能是错误。

我们编写的代码告知自己那里存在一个错误。

校验和不匹配,数据已损坏。” 在解压缩之前,请检查压缩数据块的校验和。通常,文件系统上的数据损坏时会出现此错误。由于各种原因,重新启动服务器时,某些文件被视为垃圾文件。

但是这是另一种情况:我手动读取文件,校验和匹配,没有错误。一旦出现,该错误将在重复请求后稳定地重现。服务器重新启动时,错误会消失一会儿,然后再次稳定地出现。

也许问题在RAM中?典型的情况是当比特跳动时。我看着dmesg(kern.log),但没有机器检查异常-它们通常在RAM出问题时写入。如果服务器的RAM耗尽,那么不仅我的程序无法正常工作,而且其他所有程序都会随机产生错误。但是,此错误没有其他表现形式。

“ LRUCache变得不一致。其中一定有一个错误。”这是代码中明显的错误,我们正在用C ++编写-也许是内存访问?但是CI中的AddressSanitizer,ThreadSanitizer,MemorySanitizer,UndefinedBehaviorSanitizer下的测试什么都没有显示。

也许一些测试用例不包括在内?我使用AddressSanitizer收集服务器,在生产环境中运行它-它什么也没发现。一段时间以来,通过重置某些标记缓存(香囊缓存)来清除错误。

其中一项编程规则说:如果不清楚错误是什么,请仔细查看代码,希望在那里找到一些东西。我这样做了,发现了一个错误,将其修复-它没有帮助。我在代码中的另一个地方查看-还有一个错误。更正后,再次没有帮助。我修复了一些,代码变得更好了,但是错误仍然没有消失!

原因。尝试按服务器,按时间,按负载的性质查找模式-没有任何帮助。然后他意识到问题只在其中一个集群上表现出来,而在其他集群上却没有表现出来。该错误不会经常出现,但始终在重新启动后出现在一个群集上,而在另一群集上则一切正常。

原来,原因是在“问题”集群上,他们使用了一项新功能- 缓存字典他们使用手写的内存分配器ArenaWithFreeLists我们不仅使用C ++编写代码,而且还看到了某种自定义分配器-我们两次陷入困境。

ArenaWithFreeLists是内存的一部分,在内存中连续分配大小,该内存大小可分为两个整数:16、32、64字节。如果释放了内存,则它们将形成一个由空闲的FreeLists块组成的单链接列表。

让我们看一下代码。

class ArenaWithFreeLists
{
    Block * free_lists[16] {};
    static auto sizeToPreviousPowerOfTwo(size_t size)
    {
        return _bit_scan_reverse(size - 1);
    }

    char * alloc(size_t size)
    {
        const auto list_idx = findFreeListIndex(size);
        free_lists[list_idx] ->...
    }
}

_bit_scan_reverse在开头使用带下划线的函数
一条不成文的规则是:“如果一个函数的开头有一个下划线,请阅读一次该文档,如果有两个,则阅读两次。”
我们收听并阅读了以下文档:int _bit_scan_reverse(int a)。将dst设置为32位整数a中最高设置位的索引。如果没有在a中设置位,则dst是未定义的。“我们似乎发现了一个问题。

在C ++中,这种情况对于编译器而言是不可能的。编译器可以使用未定义的行为(此“不可能”)作为优化代码的假设。

编译器没有做错任何事情-它会诚实地生成汇编指令bsr %edi, %eax。但是,如果操作数为零,则该指令bsr不是在C ++级别而是在CPU级别具有未定义的行为。如果源寄存器为零,则目标寄存器不会更改:输入中有一些垃圾,该垃圾也将保留在输出中。

结果取决于编译器在何处放置此指令。有时带有此指令的函数是内联的,有时不是。在第二种情况下,将出现以下代码:

bsrl %edi, %eax
retq

然后,我查看了一个类似的二进制代码示例objdump



根据结果​​,我发现有时源寄存器和目标寄存器是相同的。如果为零,那么结果也将为零-一切都很好。但是有时寄存器不同,结果将是垃圾。

该错误如何表现出来?

  • 我们使用垃圾作为FreeLists数组中的索引。而不是数组,我们转到某个较远的地址并获得内存访问权限。
  • 我们很幸运,附近几乎所有地址都充满了缓存中的数据-我们破坏了缓存。缓存包含文件偏移量。
  • 我们以错误的偏移量读取文件。从错误的偏移量中,我们得到校验和。但是没有校验和,但有别的东西-该校验和将与以下数据不一致。
  • 我们收到错误“校验和不匹配,数据已损坏”。

幸运的是,没有数据被破坏,只有RAM中的缓存被破坏了。我们立即收到有关该错误的通知,因为我们对数据进行了校验和。该错误已于2015年12月27日更正,并开始庆祝。

如您所见,错误的代码至少可以得到修复。但是,如何修复硬件中的错误?

铁虫


这些甚至不是错误,而是物理定律-不可避免的影响。根据物理定律,铁不可避免地是越野车。

非原子写入RAID。例如,我们创建了RAID1。它由两个硬盘驱动器组成。这意味着一台服务器是分布式系统:数据被写入一个硬盘驱动器和另一个硬盘驱动器。但是,如果将数据写入一张光盘而在刻录到第二张光盘时却断电怎么办? RAID1阵列上的数据将不一致。我们将无法理解哪个数据正确,因为我们将读取一个字节或另一个字节。

您可以通过放置日志来解决此问题。例如,在ZFS中,此问题已解决,但稍后会进一步介绍。

HDD和SSD上的位腐烂。硬盘和SSD上的位可能会变坏。现代SSD,特别是那些具有多层单元的SSD,旨在确保单元不断恶化。纠错码有帮助,但有时单元会恶化得越来越厉害,甚至无法保存。获得未检测到的错误。

RAM中的位翻转(但是ECC呢?)。在服务器的RAM中,位也已损坏。它还具有纠错码。发生错误时,通常可以从dmesg中Linux内核日志中的消息中看到它们。当有很多错误时,我们将看到类似的内容:“已修复了数百万个内存错误。”但是不会注意到单个位,并且可以肯定有些东西会出现故障。

位在CPU和网络级别翻转。在CPU级别,CPU缓存中以及在通过网络传输数据时,当然存在错误。

铁错误通常如何体现?票证“ 格式错误的znode阻止ClickHouse启动来到GitHub -ZooKeeper节点中的数据已损坏。

在ZooKeeper中,我们通常以纯文本形式编写一些元数据。他有毛病-“ 副本 ”写得很奇怪。



由于代码中的错误,很少发生更改。当然,我们可以编写这样的代码:我们使用Bloom过滤器,更改某些地址的位,错误地计算地址,更改错误的位,它落在某些数据上。就是这样,现在在ClickHouse它不是“ 复制” “但REPLI b ”并在其上所有的数据是错误的。但是通常,一点一点变化就是铁问题的征兆

也许您知道位抢占的例子。Artyom Dinaburg 进行了一项实验:Internet上的域名流量很大,尽管用户不是自己去访问这些域名。例如,这样的域FB-CDN.com是Facebook CDN。

Artyom注册了一个相似的域(以及许多其他域),但做了一点改动。例如,用FA-CDN.com代替FB-CDN.com。该域未在任何地方发布,但流量来了。有时FB-CDN主机是写在HTTP标头中的,由于用户设备RAM中的错误,该请求转到了另一个主机。具有错误校正功能的RAM并非总是有用。有时,它甚至会干扰并导致漏洞(有关Rowhammer,ECCploit和RAMBleed的信息)。
结论:总是自己对数据进行校验和。
写入文件系统时,校验和不会失败。通过网络传输时,还要进行校验和汇总-不要期望那里存在任何校验和。

更多错误!


生产集群指标用户响应请求有时会出现异常:“校验和不匹配:数据已损坏”-校验和不正确,数据已损坏。



错误消息显示详细数据:期望的检查量,该数据中实际存在的检查量,检查检查量的块的大小以及异常上下文。

当我们从某个服务器通过网络接收到数据包时,出现了一个异常-看起来很熟悉。也许再次穿越记忆,种族状况或其他。

该例外出现在2015年。该错误已修复,不再显示。在2019年2月,他突然再次出现。当时我正参加一个会议,我的同事们处理了这个问题。每天使用ClickHouse在1000台服务器中多次重现该错误:无法在一台服务器上然后在另一台服务器上收集统计信息。同时,目前没有新版本。它无法解决问题,但是几天后错误本身消失了。

他们忘记了该错误,并于2019年5月15日再次发生。我们继续与她打交道。我要做的第一件事是查看所有可用的日志和图表。他整天研究它们,什么都不懂,没有发现任何模式。如果问题无法重现,唯一的选择就是收集所有案例,寻找模式和成瘾。也许Linux内核无法在处理器上正常工作,错误地保存或加载了所有寄存器。

假设和模式


使用E5-2683 v4的9台服务器中有7台发生故障。但是,在容易出错的情况下,只有大约一半的E5-2683 v4是空的假设。

错误通常不会重复出现。除了mtauxyz群集以外,确实存在损坏的数据(磁盘上的错误数据)。这是另一种情况,我们拒绝该假设。

该错误不依赖于Linux内核 -在不同的服务器上检查,什么都没找到。 kern.log没什么有趣的,machine check exception没有消息。在网络图形中,包括转发器,CPU,IO,网络,没有什么有趣的。发生错误且未出现错误的服务器上的所有网络适配器都是相同的。

没有模式。该怎么办?继续寻找模式。第二次尝试。

我看一下正常运行时间的服务器:正常运行时间长,服务器运行稳定,存在段错误,但事实并非如此。当我看到程序因segfault而崩溃时,我总是很高兴-至少它崩溃了。更糟糕的是,当出现错误时,它会破坏某些东西,但没人注意到它。

错误按天分组,并在几天之内发生。在大约2天之内,会出现更多的情况,而在更少的时间内出现更多,然后再次出现-无法准确确定错误发生的时间。

有些错误使包裹与我们期望的支票金额相符。大多数错误只有两个软件包选项。我很幸运,因为在错误消息中,我们增加了校验和的价值,这有助于编译统计信息。

没有服务器模式我们从那里读取数据。我们校验和的压缩块的大小小于一千字节。看了十六进制的包装尺寸。这对我没有用-数据包大小和校验和的二进制表示不明显。

我没有解决错误-我再次在寻找模式。第三次尝试。

由于某种原因,该错误仅出现在一个群集上 -在弗拉基米尔DC的第三个副本上(我们希望使用城市名称来称呼数据中心)。在2019年2月,Vladimirs DC中也出现了错误,但在其他版本的ClickHouse上。这是另一个反对我们编写错误代码的假设的论点。从2月到5月,我们已经重写了3次- 错误可能不在代码中

通过网络读取数据包时的所有错误-while receiving packet from发生错误的程序包取决于请求的结构。对于结构不同的请求,不同校验和的错误。但是在错误位于相同校验和上的请求中,常数不同。

除一个外,所有有错误的请求均为GLOBAL JOIN但是为了比较,有一个非常简单的请求,压缩后的块大小只有75个字节。

SELECT max(ReceiveTimestamp) FROM tracking_events_all 
WHERE APIKey = 1111 AND (OperatingSystem IN ('android', 'ios'))

我们拒绝影响的假设GLOBAL JOIN

最有趣的是,受影响的服务器是通过自己的名字分为范围
mtxxxlog01-{39..44 57..58 64 68..71 73..74 76}-3

我感到厌倦和绝望,开始寻找完全的妄想模式。很好的是我没有使用数字命理学调试代码。但是仍然有线索。

  • 问题服务器的组与二月份相同。
  • 问题服务器位于数据中心的某些部分。在DC弗拉基米尔(Vladimir)中,有所谓的线路-它的不同部分:VLA-02,VLA-03,VLA-04。错误被清楚地分组:在某些队列中,它很好(VLA-02),在其他问题中(VLA-03,VLA-04)。

键入调试


仅使用“ spear”方法进行调试。这意味着形成假设“如果您尝试这样做会发生什么?” 并收集数据。例如,我在表中发现了一个query_log简单的错误查询,该查询的数据包大小size of compressed block非常小(= 107)。



我接受了请求,将其复制并使用clickhouse-local手动执行。

strace -f -e trace=network -s 1000 -x \
clickhouse-local --query "
    SELECT uniqIf(DeviceIDHash, SessionType = 0)
    FROM remote('127.0.0.{2,3}', mobile.generic_events)
    WHERE StartDate = '2019-02-07' AND APIKey IN (616988,711663,507671,835591,262098,159700,635121,509222)
        AND EventType = 1 WITH TOTALS" --config config.xml

在strace的帮助下,我通过网络收到了块的快照(转储)-执行此请求时收到的数据包完全相同,我可以对其进行研究。您可以为此使用tcpdump,但这很不方便:很难将特定请求与生产流量分开。

使用strace,您可以跟踪ClickHouse服务器本身。但是,该服务器可以在生产环境中工作,如果执行此操作,则会得到一系列难以理解的信息。因此,我启动了一个单独的程序来执行一个请求。对于该程序,我已经运行strace并获取了通过网络传输的内容。

该请求的执行没有错误-错误不会重现。如果转载,该问题将得到解决。因此,我将数据包复制到一个文本文件,并手动开始解析协议。



支票金额与预期相同。这正是有时在其他时间在其他请求中发生错误的程序包。但是到目前为止,还没有错误。

我编写了一个简单的程序,该程序接受一个包,并在替换每个字节中的一位时检查支票金额。程序在每个可能的位置执行位翻转,并读取检查量。



我启动了程序,发现如果您更改一位的值,您将得到那个破损的校验和,对此有抱怨。

硬件问题


如果软件中发生错误(例如,通过内存驱动),则不太可能发生单位翻转。因此,出现了一个新的假设-问题出在腺体上。

一个人可以合上笔记本电脑的盖子,然后说:“问题不在我们这边,而是在硬件上,我们不这样做。” 但是不,让我们尝试了解问题出在哪里:RAM,硬盘驱动器,处理器,网卡或网络设备中的网卡RAM。

如何定位硬件问题?

  • 问题出现并在某些日期消失。
  • 受影响的服务器按其名称分组:mtxxxlog01-{39..44 57..58 64 68..71 73..74 76}-3
  • 问题服务器的组与二月相同。
  • 问题服务器仅位于数据中心的某些队列中。

网络工程师有疑问-数据在网络交换机上跳动。事实证明,网络工程师恰好在那个日期将交换机换成其他交换机。问了一个问题后,他们用以前的替换了它们,问题消失了。

问题已解决,但问题仍然存在(工程师不再需要)。

为什么ECC(纠错存储器)对网络交换机没有帮助?因为多位翻转可以互相补偿-您会得到无法检测到的错误。

为什么TCP校验和没有帮助?他们很虚弱。如果数据仅更改了一位,则TCP校验和将始终可见。如果两位已更改,则更改可能不会被检测到-它们彼此抵消。

我们的程序包仅更改了一点,但错误不可见。这是因为TCP段中的2位发生了变化:他们从中计算出校验和,这是一致的。但是在一个TCP段中,我们的应用程序存在多个数据包。对于其中之一,我们已经考虑了校验和。该数据包中只有一位发生了变化。

为什么以太网校验和没有帮助-它们比TCP强吗? 以太网支票金额检查数据汇总,以使它们不会在通过一个网段的传输过程中中断(我可能会用错术语,我不是网络工程师)。网络设备转发这些数据包,并可以在转发过程中转发一些数据。因此,支票金额可以简单地重新计算。我们检查了-线上的包装没有改变。但是,如果他们击败了网络交换机本身,它将重新计算检查量(将有所不同),并进一步转发数据包。
没有什么能拯救您-对自己进行校验和。不要指望有人为您这样做。
对于数据块,将考虑使用128位校验和(以防万一,这种过大的杀伤力)。我们会正确告知用户有关错误。数据是通过网络传输的,虽然已损坏,但我们不会在任何地方进行记录-我们所有的数据都井井有条,您不必担心。

ClickHouse中存储的数据保持一致。在ClickHouse中使用校验和。我们非常喜欢校验和,因此我们立即考虑了三个选择:

  • 对于写入文件和网络时的压缩数据块。
  • 总检查是用于对帐验证的压缩数据之和。
  • 总检查是用于对帐验证的未压缩数据的总和。

数据压缩算法中有错误,这是一个已知情况。因此,在复制数据时,我们还要考虑压缩数据的总校验和和未压缩数据的总量。
不要害怕计算支票金额,它们不会放慢速度。
当然,这取决于哪些以及如何计算。有细微差别,但请务必考虑支票金额。例如,如果您从压缩数据中进行计数,那么数据将更少,它们不会减慢速度。

改进的错误信息


当用户收到这样的错误消息(这是硬件问题)时,如何向用户解释?



如果校验和不匹配,在发送异常之前,我会尝试更改每一位-以防万一。如果更改时校验和收敛并且更改了一位,则问题很可能是硬件。

如果我们可以检测到此错误,并且如果在更改一位后更改了该错误,那为什么不解决它呢?我们可以这样做,但是如果我们始终纠正错误,用户将不会知道设备有问题。

当我们发现交换机存在问题时,其他部门的人员开始报告:“我们写了一些错误的信给Mongo! PostgreSQL中有一些东西给我们!”很好,但是最好早点报告问题。

当我们发布新的诊断版本时,第一个为其工作的用户在一周后写道:“这是消息-有什么问题?” 不幸的是,他没有阅读。但是我以99%的概率阅读并建议如果错误出现在一台服务器上,则问题出在硬件上。我留下了剩余的百分比,以防万一我写的代码不正确-发生这种情况。结果,用户更换了SSD,问题消失了。

数据中的“ Delirium”


这个有趣而出乎意料的问题使我担心。我们有Yandex.Metrica数据。一列中的一个将简单的JSON写入数据库-来自计数器JavaScript代码的用户参数。

我发出某种请求,而ClickHouse服务器因segfault崩溃。从堆栈跟踪中,我意识到了问题所在-来自另一个国家的外部贡献者的新承诺。提交已修复,segfault消失了。

我运行相同的请求:SELECT在ClickHouse中,获取JSON,但同样,废话,一切运行缓慢。我得到JSON,它是10 MB。我将其显示出来,{"jserrs": cannot find property of object undefind...然后更专心地看:然后产生了一个兆字节的二进制代码。



曾经有人认为这又是记忆力或比赛条件的转折。许多这样的二进制数据是不好的,它可以包含任何东西。如果是这样,现在我将在那里找到密码和私钥。但是我什么也没找到,所以我立即拒绝了这个假设。也许这是我在ClickHouse服务器上的程序中的错误?也许在一个编写程序(它也是用C ++编写)的程序中-突然她不小心将其转储内存放入ClickHouse了吗?在这个地狱里,我开始仔细看这些字母,意识到这并不那么简单。

线索路径


相同的垃圾记录在两个群集上,彼此独立。数据是垃圾,但是它是有效的UTF-8。此UTF-8包含一些奇怪的URL,字体名称和很多字母“ I”。

小西里尔字母“ I”有什么特别之处?不,这不是Yandex。事实是,在Windows 1251的编码中,它是第255个字符。在我们的Linux服务器上,没有人使用Windows 1251编码。

事实证明,这是浏览器的转储:指标计数器的JavaScript代码收集JavaScript错误。事实证明,答案很简单- 全部来自用户

从这里也可以得出结论。

来自Internet的错误


Yandex.Metrica收集来自Internet上10亿个设备的流量:PC,手机,平板电脑上的浏览器。垃圾将不可避免地出现:用户设备中存在错误,到处都有不可靠的RAM和可怕的硬件过热。

该数据库存储了超过30万亿行(页面浏览量)。如果您分析此表中的数据,则可以在其中找到任何内容。

因此,在写入数据库之前先过滤掉这些垃圾是正确的。无需向数据库中写入垃圾-她不喜欢它。

HighLoad++ ( 133 ), - , , ++ PHP Russia 2020 Online.

Badoo, PHP Russia 2020 Online . PHP Russia 2020 Online 13 , .

, .

All Articles