OK Log 姐妹篇

OK Log是一个用于大规模集群的分布式且无协的日志管理系统。我是从一些最基本的原则考虑这个系统的设计的。下面介绍的就是这个原型的思路来源。

绪论

过去的一两年时间,我受邀参加很多关于微服务、Go和的演讲和研讨会议。选一个微服务架构,意味着要对很多考虑点进行技术选型。如果可能的话,对一些新兴的中等规模系统,我愿意给出一些技术指南。开源社区的项目是非常丰富的。

  • 服务编排? 有 Kubernetes nomad 、DC/OS、ECS等,有很多服务编排工具,都是很好的选择。(ps:目前docker和Kubernetes深度合作了,Mesos可能要被边缘化了)。

  • 服务发现?  Consul Etcd Zookeeper 动态服务发现等工具,也有静态注册和服务发现工具 Linkerd

  • 分布式调用链跟踪? Zipkin Jaeger 、Appdash、Lightstep等,它还在爆发式增长.

  • 监控工具? Prometheus , 它是目前最强的监控工具、 InfluxDB 等,它们结合 Grafana 工具使用

  • 日志?我陷入了沉思....

很明确的答案似乎是Elastic和ELK技术栈。确实,它很有特点、且入手很容易。但是Elastic被很多人认为,对于中等规模的集群,都很难操作。同时我相信,在全文、基于文档搜索时,Lucene或许不是最好的的数据存储格式。最终,我了解了很多使用Elastic的朋友,由于操作的难度很高,他们中的大多数都不怎么乐意使用它。几乎很少有人使用更高级的特性。

更美好的事物

我认为,对于日志管理系统,应该有一个更好的答案。我问了一些同事,他们正在着手的解决方案。一些同事实际上采用了Kafka消息队列解决日志系统管理,特别是对于高QOS和持久化日志要求。但是它的操作也相当难,且最终设计成的日志管理系统,和我感兴趣要解决的问题也不相同。其他人通过数据仓库HBase来解决。但是管理一个Hadoop集群需要更加专业化的只是和非凡的努力。对于这些方案的选择,我认为具体化的或者比较重的系统设计都是一个好的建议。

我还在Twitter上提出了这个问题。Heka似乎是最接近我需要的,但是因为作者前期设计错误,导致了16年年底遇到了无法修复的性能问题,已经放弃了Heka的维护,这是一件非常糟糕的事情。Ekanite提供了端到端的解决方案,但是它的系统日志协议与微服务的工作负载有很明显的不匹配。对于日志传送和注解有非常好的工具,例如:Fluentd和Logstash,但是它们只能解决部分问题;它们不能处理存储和日志查询。委托解决方案的工具,有Splunk和Loggly,如果你的日志是低容量,且不介意把日志上传到云端,这两个工具都是很好的选择, 但是它们很快变得昂贵,且无法再本地和开放源代码框中打勾。 (ps: 这句话不是很明白)。

Prometheus日志

我意识到我需要的是Prometheus日志的设计原则。什么意思呢?Prometheus好的地方有什么呢?我的观点:

  • 独立运行:它既是开源的、又可以在本地部署

  • 云原生 的工作负载:动态的、容器化的和微服务的水平扩展. (ps: 链接中的解释我是非常满意的,是不是就是Serverless)

  • 容易操作:本地存储、没有集群、拉模式

  • 完善的系统:不需要独立的TSDB(时间序列数据库)、web UI等,容易使用

  • 系统扩容:90%的用户承认使用很小的成本,就可以获取比较高的满意度

那Prometheus日志是什么样子的呢?我希望冬天把这个日志管理系统设计完成,我认为这是非常有趣的,同时我也可以学到很多的知识。首先我需要思考得更加深入。

设计

高层次目标

首先,像Prometheus一样,系统应该是开源的,且支持本地部署。更重要的是,它应该很容易部署和水平扩展。它应该更加关注容器化的微服务工作负载。同时他应该是一个完善的,端到端的系统,有forwarders、ingesters、storages和query四个特性。

这个日志管理系统关注点:

  • 微服务的应用程序日志,包括:debug、info、warn等各种级别日志。这个是典型的高容量、低QOS日志,但是对延时(查询时间)有较高的要求。

  • 我们也想服务于事件日志,包括:审计跟踪和点击跟踪等等。这是典型的低容量,搞QOS,但是对延时(查询时间)没有较高的要求。

  • 最后,它应该有一个统一的日志消费者,管理来自黑盒的日志输出,例如:mysql服务。也就是说,我们不会控制日志格式。 我相信这样的系统可以服务于所有的需求,同时扩展性也非常好。

心里有了这些目标,我们就需要开始增加一些约束,有了边界才能使问题更加容易处理,关注点更加集中。

问题约束

宝贵的经验告诉我,数据系统应该更多地关注数据传输,同时增加数据的价值。这就是说:

  • 它是一个数据运输系统,解决更多的机械问题,黑盒运输

  • 它也应该是一个应用系统,提供商业价值,对拓扑和性能要求不需要参与

如果尝试用一个方案解决这两个问题,会造成竞争和一定的妥协。所以我比较感兴趣数据传输系统,旨在解决低吞吐率和延时问题。我们可以使用其他的工具,在系统外部增加数据的商业价值。例如:上下文context可以在ingest之前发生。或者,解析日志再聚合可以在ETLs(数据仓库技术)中完成。然后再使用更加丰富的查询功能的数据系统将其结果视图化。

考虑到这一点,基于时间边界的grep查询接口是完全可接受的。对于新用户,他们经常想要一个熟悉的接口来帮助他们调试-“我想要grep我的日志”,这是非常有用的。构建ETLs(数据仓库技术)到更复杂的系统中是完全足够的。总之,这个日志管理系统是一个基本的、底层系统,它可以和其他工具搭配使用,至于搭配什么样的工具,主要看你自己的需求。(ps: 类似于系统插件化)

分布式系统

去年在旧金山的Prometheus见面会上,Julius Volz观察到日志数据比监控数据要大几个数量级。Prometheus安装的大多数节点日志已经超出了ingest和单节点容量限制。因此,与Prometheus相同的日志系统必须是一个分布式系统。这个复杂度是根本性的、不可避免的。那好,我们就着手解决它。

无协同

到目前为止,我们最重要的目标是系统容易操作。并且从Prometheus中,我们学习到它应该是平滑水平扩展的,从测试环境到生产环境,没有重大结构变化。在简单和复杂的系统设计中做出合适的权衡是非常痛苦的。但是我强烈建议无协同系统。无协同意味着放弃了其他软件系统的一些优秀特性,如:Elastic、Kafka和Canssandra等。没有master选举、没有节点分配、没有分区表、没有分布式索引、没有vnodes。承认暂停、分袂和死节点是这个设计的一部分。某种意义上来说,这些会使系统设计更加困难,我们使用很少的技术做支撑,所以需要花更多的时间做前期设计。但是另一方面,它更容易实现,因为无协同组件往往更简单,更容易实现。

我们可以看到,如果我们能够设计一个组件模型是无协同的,那么我会充分思考系统的设计

写的重要性

在我们开始设计之前,一个重要的观察:在日志管理系统中,写需求是强烈的,读需求可以等待。(ps: 因为写阻塞,会影响业务系统的性能)。所以,我认为最重要的运行时挑战是写高吞吐量。理想情况下,它无限接近到硬件速度————这也有助于日志管理系统的节点容量扩展。

首先日志系统的总体设计。Agents把日志记录从容器forward到ingesters中。这个ingesters应该执行快速的序列化写操作,把日志记录写入到一个活跃的段文件中,这个ingesters的任务就完成了。让存储节点更关心读操作的优化。

Ingestion

由于我们对ingestion和querying有不同的性能要求,那么分离这些组件是非常有意义的。它们是细粒度的节点安装。我们也可以通过一个编译过的二进制文件安装,这样更加容易。

在写操作的时候,每条日志记录被ingester赋予一个全局唯一的ID。这使得很多特性的实现变成了可能,例如:多次相同日志记录的去重。唯一的ID生成有很多方式,例如:UUID或者ULID, 还有twitter提出的64位byte[毫秒数:业务线:机房:机器:预留:毫秒内序列号],这些都非常好。有一点非常重要,每条日志记录有个合理精度的时间戳去创建一个全局唯一的ID;但是有一点不重要的,时钟是全局同步的,或者这些日志记录是严格的线性递增。同时,我认为如果在同一个相同的时间窗口内出现了乱序,只要顺序是稳定的,这也是ok的。ULIDs曾明能够在50ns内生成ID有序,它可以很好的工作。

为了满足日志持久化要求,这里有不同的持久化模式。

  • 如果我们主要关心吞吐量,例如:应用程序的日志,我们可以使用fast模式。写入一个文件描述符而不需要直接同步;

  • 对于事件日志,有一个持久化模式,我们定期地同步活跃段到磁盘上;

  • 最后一种批处理模式:许多客户端同时写整个段文件,只有当它完全复制到其他节点时才被确认。(这个是从Kafka获取的灵感)

这样,我们的日志管理系统的组件模型慢慢的变成了下面这幅图:

我们可以对协同的讨论思考得更多一点。如果我们编排它,以至于任何ingest节点都能够服务于任何forwarder传送的日志记录,这样我们避免了forwarders需要知道超出ingesters的地址信息。forwarders是在ingesters池中任意拿一个进行连接,并且能够实现反压(backpressure)、缓冲和重连逻辑等。依赖于其他下游服务。有了这些限制,ingesters可以不受约束。到目前为止这种方式还挺好的。

日志复制

日志记录写入磁盘是不安全的,一旦落到磁盘,日志记录就需要被复制备份。我们知道数据需要多节点存储。

来自Prometheus的日志设计思路,我们把典型的日志复制有push模式改为了pull模式。准确地说,一个集群中的所有ingest和存储节点,需要通过gossip协议通信。所有的存储节点定期地和随机地消费来自所有ingest节点的段文件。消费后的段需要合并,同时合并之后的文件达到一定的size后,就会复制到其他存储节点上进行备份。只有成功复制之后,原始段从ingest节点中确认和清除。

实际上,我们把每一个ingest节点变成了一个后端磁盘队列。并且每一个存储节点获得了整个日志的子集,并且密度由复制因子决定。

为什么要把ingest节点数据移动到存储节点上呢?

  • 在较小的场景中,低频的读操作和写操作负载可能这是没有什么作用。例如:本地测试,我们会提供ingest+存储的混合节点

  • 在一些较大的场景中,I/O可能是最主要的瓶颈,并且ingest工作负载(顺序写)与存储工作负载(半随机读和写)是竞态的。隔离是很聪明的做法。

在复制事务期间,任何失败(或者超时)都会造成事务的中断,并且ingester段将会在后面重新消费。这回造成重复日志记录,但是这是ok的。因为查询时结果通过ULIDs是去重的。最终,我们至少交付了一次。这种复制形式是事务的,但是没有协同。

弹性

注意到,ingest层实际上是一个分布式的,磁盘存储的日志记录队列。我们能够扩展ingesters来处理我们的写容量。同时我们也能扩展存储层来处理我们的复制因子,设置日志有效期,以及读容量要求。

增加节点到每一层,就像让他们加入集群并开始工作一样简单。有一个优化,ingest节点能够通过gossip协议扩散负载信息,并通过增减来平衡节点负载。存储节点自动地开始消费来自多个ingest节点的段共同平分的份额。只要ingest段size小于存储段size,就可以立即平衡写负载。磁盘利用率在保留时间范围内保持平衡。所有这些都没有明确的成员注册,芈月空间声明或者任何形式的公式。集群的增长或者削减都是无协同的。

合并

存储节点最终累计不同size和时间范围的段文件。合并是对日志记录的清洗、合并和重新分割的过程,目的是统一数据格式存储和优化查询。

合并能够merge段的叠加,如上图所示,合并小的、序列化的段。在每一个阶段,它可以炒作统一的段文件数据格式一步步进化,这就是我们想要的时间边界查询。同时,合并agent能够用于强制保留期。观察数据集保持不变,只有磁盘上的布局优化。合并的影响是透明的,本地的,所以无协同。

查询

查询字面上是时间边界grep。我们分散查询到所有的查询节点上,然后聚集数据,返回合并后且日志去重的记录给用户。每一个ULIDs日志记录为去重的日志记录提供可排序的身份ID。通过从较少的节点读取,可以提交效率吗?Yes。 but that would involve prior knowledge about segment location/allocation, which requires some form of coördination. We deliberately make the read path dumb, and pay some costs of inefficiency, to keep it coördination-free ., (ps: 这段话不明白)

原型设计

实现

在几个朋友的帮助下,我逐渐详细地描述了系统设计。这给我带来了很大的乐趣。设计无协同的分布式系统是人生中一个非常大的乐趣。经过几周时间的努力,我开始说服自己,设计方案是可行的。经过整个假期,我开始了一个设计代码实现。经过一周或者更久,我有了一个看似正确且有用的原型。然后开始花时间进行压力测试。

验证

现在我将会描述验证的过程,并且通过连续的系统负载测试来分析系统性能。这个测试环境是由DigitalOcean提供的,在此感谢他们!

我创建了8个forwarder节点集合,3个ingester节点和3个存储节点。我开始从一些基本的正确性和crash测试入手,很快就被来自每个组件的垃圾日志所淹没。重现状态时非常困难的,或者从日志垃圾邮件中得出有意义的结论。我最终删除了一大堆日志语句,并添加了很多指标。构建Prometheus表达式和图表是建立洞察力更有效的方法。最后,我仅仅在启动时记录一些运行参数,并清除错误,如:写入文件失败。我非常清楚地意识到这里的坑。

吞吐量

我想要第一件要优化的是吞吐量。为了满足我自己的好奇心,我再Twitter上做了一个调查。我对集群中的每个节点日志吞吐量非常感兴趣。这个测试结果范围值很大,从1KBps到25MBps之间变化。5MB/sec/node对于80%~90%的方案是一个比较好的目标。让我们看看典型的测试用例。

DigitalOcean磁盘显然可以达到250MBps的持续写入,在云服务中这是表现非常好的。在我自己的测试环境中,磁盘写入测试要少一些,它在150MBps上下浮动。如果我们系统设计得正确,那么150MBps就是我们的I/O性能瓶颈。因为我们每个节点的写速度控制在5MB/sec/node,则单个ingest节点能够处理写操作不阻塞的集群节点大小范围:150/5=30 ~ 250/5=50个节点。这个范围间的集群因子都是合理的。因为我们有3个ingest节点,所以写操作的速度是150MBps*3个节点=450MBps的聚合速度。

优化forwarding

这个forwarder不过是netcat而已。基本地

   // client connection, forwarder就类似于下面的作用,传送数据
    conn, _ := net.Dial("tcp", ingesterAddress)
    s:=bufio.NewScanner(os.Stdin)    for s.Scan() {
        fmt.Fprintf(conn, "%s\n", s.Text()) // 往tcp链路中向服务端发送终端产生的数据
    }

Go's bufio.Scanner在这里非常形象;产生数据后,通过tcp链路传送数据。~~whatever limits I hit, they weren’t imposed by the scanner. ~~,(ps: 不明白)。我用一些低效率地方式来生成日志记录。我观察到CPU一路飘高,吞吐率远低于预期。性能分析暴露了两个问题:

1.我在热循环中使用了一个time.Ticker。每一个日志行带有一个ticker

hz:=time.Second / recordsPerSecondfor range time.Tick(hz) {// 传送一条日志记录}

这里有一个问题,如果你想要每秒记录1000条日志时,则每1ms阻塞一次是资源浪费的。我推荐采用批量传送日志记录,如下所示:

var (
recordsPerCycle = 1timePerCycle  = time.Second / recordsPerSecond
)for timePerCycle < 50*time.Millisecond {
recordsPerCycle *= 2timePerCycle *= 2}for range time.Tick(timePerCycle) {// 每次记录recordsPerCycle条日志记录}

2.我利用随机数据在热循环中构建每一行日志,大量消耗CPU。在程序开始时,预先计算一大组固定的随机日志可以解决这个问题。通过这些变化,我可以很轻松地从每个进程推送大量的MBps,而且负载可以忽略不计。这个第二点翻译感觉很有问题, 原文如下:

Also, I was building each log line, of random data, within the hot loop, and burning lots of CPU in math.Rand to do it. Precomputing a large, fixed set of random log lines at program start solved that one. With those changes, I could easily push plenty of MBps second from each process with negligible load.

我为每个forward节点创建了1-8个forwarders,共为8个ingest节点设置了8-64个forwarders。每个进程将每秒处理100-1000条日志记录,每条日志记录有100-8000个bytes,每秒生成能力高达512MB。非常高的性能。

优化ingestion

在开头我很担心每一条记录一个全局唯一ULID造成的性能问题。但是多亏Tomás Senart写的优秀库ULID,这些代价实际上非常低,每ULID消耗50ns,则1s可以生成1000000000/50=2千万个ULID。因为我们不需要任何数据加密协议。下面是测试性能数据:

BenchmarkNew/WithoutEntropy-8    30.0 ns/op  534.06 MB/s  1 alloc/op
BenchmarkNew/WithEntropy-8       65.8 ns/op  243.01 MB/s  1 alloc/op
BenchmarkNew/WithCryptoEntropy-8  771 ns/op   20.73 MB/s  1 alloc/op

我最初能够将每个ingest实例推送到30MBps,但是事情变得很棘手。初始化性能分析揭露了在 bytes.FieldsFunc 和系统调用方法中,CPU不成比例的过度消耗。

  • 对于 bytes.FieldFunc 方法,我对比了ULIDs,前者表现令人意想不到的差劲。切换到固定大小偏移分割-ULID给了我们这个能力,改进并提升性能,但不是ingest率。

  • 对于系统调用syscalls,我怀疑是文件系统的竞争导致的。为了验证我的怀疑,我抽象了文件系统,并提供了一个无操作的实现。即使扩展到几百兆的ingest节点写入,这性能表现也非常好。事实证明,在最初的设计中,我将所有传入的连接多路复用到相同的活跃段文件中。假设将部分段传送到存储节点会是一个瓶颈,所以我的想法是优化(最小化)每个ingest节点产生的活跃部分数量

但是性能分析揭示,多路复用会花费很大的代价。因此,我分离它,把每个连接写入到自己的段文件中。这是最主要的改善。

活跃的段文件达到一定的时间或者size就会写入到存储节点,并关闭段文件。这个时间限制保证了系统日志的实时性,文件大小保持复制可管理特性。

  • 如果根据时间窗口关闭段文件,日志记录大小可能会比较低,而且事情也简单。我们能够基于想要的日志记录延时,选择你想要的时间窗口。(ps:但是这可能会造成日志持久化不实时,当日志产生后查询不到 果)。 3s是默认值

  • 如果根据size大小关闭段文件,我们会有高容量,但是如果日志记录产生速度过慢,则会造成段文件在一定时间内达不到size阈值,造成上面一样的结果,不实时。

所以我们必须要进行时间和size的权衡,文件应该足够大,以便分摊复制开销,并有效利用典型的SSD,但又足够小,可以快速传输,并有效地组成存储层段文件。我选择16MB作为默认的ingest段文件大小,同时选择128MB作为存储节点段文件的默认大小。但是这些选择没有必要是统一的。他们可能会极大地影响系统吞吐量,应该要在特定的环境中进行验证。

随着ingest节点积极地消费和保存记录,下一步我们讨论存储节点。

优化复制

日志复制工作起来要像这样:一旦一个ingest节点的段,由于时间窗口或者size关闭了,那么存储节点则可以存储该段了。存储节点随机轮询ingest节点,并且获取最老可用的段(ps: 偏移位置)。它们会在内存中就合并好这些段到一个聚合的段中。这都是在一个循环中做的,直到这个聚合段达到了设置的时间窗口或者size。然后它们会复制这个聚合段到N个随机的存储节点上,N是由你的复制因子决定的。一旦复制确认完成,这个ingest节点的段将被确认和删除。

在实践中这个复制机制工作得很好。甚至当ingestion真的不平衡时,存储节点也会消化掉要复制的日志段。

存储节点通过状态机consume段文件:gather、replicate和commit。

  • gather状态要求尽快地调整并消费这些段文件(当然,我明白轮询很糟糕,订阅方式比较好。未来我会实现这个功能)

  • replicate状态主要是采用POST方式发送聚合的段到其他存储节点。用足够大的段来分摊这个POST的成本也是很重要的。(我们也可以通过一直选择本节点作为目标节点之一来进行优化。这也是未来的工作)

这里成功的标准是看ingest节点数在队列上的深度。。换句话说,就是存储节点的消费日志的速率与ingest节点接收日志的比率。

这个比率保持在1附近上下浮动是最好的。既不会饥饿,也不会撑死。大于1,则表示存储节点的消费速度大于ingest节点的接收速度;~~maybe chewing through a backlog? ~~ (ps: 不明白)。小于1,则表示存储节点消费得不够快,最好是添加存储节点。

最初的这些设计证明是稳定可靠的。当然还有很多可以优化的地方,但是还没有发现重大问题。我很满意整个系统处理日志的速率。但是它能查询吗?

优化querying

这个是真正的考验。如果查询速度太慢,大多数用户将无法忍受。初始的查询性能测试完全达不到我的预期。现在这部分仍然还有很多可以改善的地方。

初始的 grep 设计师非常简单的。我们通过读取多条日志记录,然后通过多路归并算法找到匹配的段日志记录。并且我们在这个归并的输出上附加上了时间范围和查询表达式过滤的条件查询。整体查询框架图如下:

Tomás注意到,在一个完美紧凑的系统中,不会有重叠的段。在这种情况下,不需要过多消耗CPU来完成全局的归并。我们可以首先读取不重叠的段,然后序列化地读取重叠段先进行一次归并。(ps: 可以这样理解,先把不重叠的段不合并,重叠的段先局部合并,然后再一次次的局部合并,最后在做一次整体合并)因此,我实现了一个MultiReader,它由普通的文件reader或者归并reader组成。具体取决于段的重叠。

利用这种方式,在某些例子中可以提高50%的速率。

然后我们再收集了一些性能分析数据,显示在系统调用syscalls、日志记录的过滤管道和正则表达式匹配时的CPU消耗。我们认为显示的文件mmaping可能会提高读性能,所以我们设计的一个mmaping文件系统抽象的原型。但是和直接的文件系统做性能对比,我们没有获取显著的性能改善。

同时我也对比了 过滤日志记录从磁盘读取日志 的时间开销。对比图如下所示:

  • 在直接的文件系统中,读取时间开销主要在系统调用;

  • 在mmap文件系统中,读取时间开销主要在memmove上。

我发现页面缓存是非常有效的

经过一两天的思考,我意识到操作的顺序是归并然后过滤

上面这幅图,有一部分CPU消耗在了不相关的段归并上。由于归并建立在一个全局有序的事务上,所以它被约束在一个CPU核上来完成。如果我们先过滤,然后再归并过滤后的段。这使得前者可以并发执行,充分利用多核特性。

做了这些改变后,查询速度比之前高两倍了。CPU利用率更高了。但是阻塞分析揭示了在每个段的goroutine写入到io.Pipe中需要花费大量的等待时间。如果我们可以缓冲pipe,我们会有更好的性能提升。但不幸的是,io.Pipe没有给你一个可以缓冲的内存空间。同时,幸运的是,我们找到了 djherbis/nio 包,它提供了一个具有相同功能的缓冲Pipe。用一个适中的1MB缓冲区与直接用io.Pipe进行速度对比,提高了2倍多,太惊人了!!!

满意后,我们开始把注意力放在过滤时附加的正则表达式时间消耗。切换到 bytes.Contains 后有了合理的改善。事实证明各个点优化也是非常nice的。因此,我给查询时间定义了这个flag,只有在需要的时候才选择加入正则表达式匹配。(进一步优化,可能会使用PCRE(ps: 这个perl写的正则表达式库比其他库的速度要提高3倍)正则表达式,这是未来一段时间的工作)。

此时,我们意识到,我们当前的工作集(大约21G)超过了存储节点(8G)。如果我们可以获取更多的内存,我可以一次性加载整个工作集到页面缓存中,并希望借此解决其他的低效率问题。我们启动了一个32G DigitalOcean droplet,包括更多的CPU核来协助并发过滤。没有其他的变化就给了我们两倍的提速。

之前调优Cassandra的经验给了我们更多的想法。我们调整了I/O调度和readahead设置,这给了我们另外20%的改进。尽管在这一点上,我们已经非常接近仅仅基于节点上的内存总线。我们在4.6s内可靠的查询了20G日志记录。读取吞吐量为4.47GBps。这里可能还有额外的工作来优化磁盘访问,但是这似乎完全可以达到初始设置的标准。

版本一

现在大家使用的就是版本一!OK Log。哪还有什么工作要留给未来呢?

未来工作

我们能够,而且应该做类似于Cassandra的一些读修复。即查询结果在所有存储节点的数据都是相同的。存储节点数据不一致的日志记录目前还不能被检测、批处理和重写到存储节点中。未来合并会把他们最终存储到合适的位置。这个是issue 6

```Cassandra读修复 Cassandra读修复

客户端读取某个对象的时候,触发对该对象的一致性检查:

读取Key A的数据时,系统会读取Key A的所有数据副本,如果发现有不一致,则进行一致性修复。

  1. 如果读一致性要求为ONE,会立即返回离客户端最近的一份数据副本。然后会在后台执行Read Repair。这意味着第一次读取到的数据可能不是最新的数据;

  2. 如果读一致性要求为QUORUM,则会在读取超过半数的一致性的副本后返回一份副本给客户端,剩余节点的一致性检查和修复则在后台执行; 3.如果读一致性要求高(ALL),则只有Read Repair完成后才能返回一致性的一份

  3. 数据副本给客户端。可见,该机制有利于减少最终一致的时间窗口。

  4. 相关地,在数据丢失之前,整个系统能够容忍N-1个存储节点挂掉,但是如果一个存储节点挂掉了,没有其他修复进程拉起和修复数据的话,我们会进行服务降级。一个修复进程监听每个存储节点,当节点挂掉后会立即启动进程修复,它会顺序地查询每条日志记录,触发对尚未完全复制的日志记录进行读取修复。这个是issue 11。

添加一个ingest节点对系统来说是无感知的。仅仅只有新的客户端会连接它,存储节点消费它,小事情。但是ingest节点会通过gossip协议传送各自的瞬时负荷给通信的对方。例如:连接的客户端数量,吞吐量等。这里有三种处理情况:

  • 有些高负载的服务器可以在连接时向新客户端提供一些轻量级的服务。客户端,例如:forward节点也会根据ingest节点的负载情况合理的传送日志记录。

  • 高负荷的ingest节点会在一段时间内拒绝新连接请求。

  • 如果需要的话,再高负载的ingest节点,也可以既拒绝新连接请求,也可以启动现有连接。

以上三种策略都是可以的,考虑到forward节点会重连集群中的其他ingest节点。有一点需要关心的是:响应慢,绝不要超过X%的ingest节点拒绝连接。这个ingest节点的负载均衡在issue 2有讨论。

相似地,增加一个新的存储节点也工作得很好。它会开始consume来之ingest节点的共享成比例段文件。在保留时间的窗口内,这将是平等的。但在该窗口过去之前,新的存储节点和其他存储节点相比,前者获取的共享比例段比较小。作为一种优化,它会通过gossip协议告诉其他存储节点它的当前数据集。当复制时,存储节点偏向于具有较小总体数据集大小的节点。新的存储节点将会获得较多的数据复制请求;老的存储节点将会接收较少的数据复制请求。这种逐渐重新平衡策略反应了我的观点,即尽可能地不要移动数据,静态存放。在发生大的拓扑变化之后,我观察到由分段/分片/节点重新平衡引起的大量中断。我认为这是可行的,但是它是否真的可行还有待观察。存储层的负载均衡在issue 3有讨论。

失败模式是经过深思熟虑的,但是没有经验证实它。我有兴趣构建一个带有故障注入的分布式验证框架,类似于简化的Jepsen风格测试工具。另外,建立一种方法来验证总体系统的吞吐量(MBps)和延时(ingest to query)。这个测试工作在issue 14中讨论。

在早期的设计过程中,我观察到队列理论在这里比较适用。我真的很喜欢用Adrian Cockcroft的微服务响应时间分布式的分析方法对系统进行建模。我开始着手这个工作,但是我没有太多时间去跟这个事情。这个模型在issue 9中讨论。

这真的只是一个开头;还有很多其他工作量比较小的事情要做。问题清单可能还需要一两个月才能列出来。

总结

这个系统对一些人是有用的吗?我不知道,或许吧。如果有人多听说,请试一试,或者可以邀请我和你一起在线讨论。如果没有,也好,这个系统设计对我也是一种很好的锻炼,是一个很享受的过程体验,并从中学到了很多知识。对我来说这就足够了。

技术活动

年末最不容错过的云领域技术盛会

国内 Go 语言布道者 许式伟 和  Asta   联合发起

阿里云褚霸、京东云郭理靖、饿了么CTO雪峰、PingCAP创始人刘奇、七牛云技术总监陈超、Docker 专家孙宏亮

......

众星云集,为你而来

点击“ 阅读原文 ”,即可报名

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章