Jekyll2022-05-01T12:16:36+00:00http://tunzao.me/atom.xml静夜思It works on my machine.tunzaotunzao@live.cn读《居里夫人自传》2022-05-01T00:00:00+00:002022-05-01T00:00:00+00:00http://tunzao.me/articles/2022/05/01/thoughts-on-autobiography-of-marie-curie<p>看到“自传”二字的时候我的第一反应就是这些名人都这么把自己当回事儿了吗?不过在第一章第一节中居里夫人就回答了我的疑惑:“刚开始的时候,我觉得这个建议(写自传)对我来说真的是难以接受”。后来想了下其实 Michelle Obama 的<a href="https://book.douban.com/subject/30232128/">《Becoming》</a>也是自传啊,当时阅读时就没有这么强烈的质疑,原因大概是我对书籍类型的区分上还停留在书名而不是书籍内容上。</p>
<p>这本小书还是挺简短的,然而其中还有一半的篇幅是关于皮埃尔·居里的。前后两个部分的写作风格完全不同,前半部分是对居里夫人自己家庭和工作的平铺直叙,可能过于平白,让人感觉居里夫人不过就是一位天赋平平但异常刻苦努力的普通人;后半部分写到皮埃尔时,就好像立马切换到了小迷妹模式,介绍皮埃尔生平的过程中不惜使用所有赞美之词对皮埃尔的为人和工作上取得的成就大加赞扬,读到接受皮埃尔留下的重担时竟然还让我泪目了;对皮埃尔工作研究的细节也描述的比较深入,不过这部分稍显苦涩的内容看得让人有点昏睡。</p>
<p>书籍的大部分内容还是关于二人生平的工作,但作为在科学史上做出过杰出贡献的科学家,也需要处理工作和生活的平衡,由于他们对财富的态度,这种平衡是要靠持续的克俭克勤来支撑的。虽然描述生活的篇幅很少,但从两位女儿后来的发展来看,工作和生活好像还平衡的不错。另外也谈到了对子女的教育,因为觉得私立学校的教育低效而不利于孩子发展,就与同事成立了兴趣组共同担负孩子们的教育,这必然会是件要占用大量精力的事情,但似乎居里夫人也还应付的不错。</p>
<p>两人的三观也正的让人顶礼膜拜。二人潜心于为人类谋幸福的科学研究,不慕名利、不追求物质财富,厌恶需要疏通关系而不是靠个人业绩才能晋升的世俗。他们是“有理想主义的人,他们追求大公无私的崇高境界,没有时间去顾及自身的物质利益。”读到居里夫人关于财富的观点,让我联想到了华大基因董事长汪健在<a href="https://v.qq.com/x/cover/hnmh8h5ux5eumgq/b00256a0k8j.html">《十三邀》</a>中说了句:“最傻的就是商人”,如果这句话从居里夫人口中说出来我大概也不会觉得意外。那么究竟是什么塑造了居里夫妇如此三观,那个时代背景也许发挥了不少作用,两个有着共同道德观的家庭环境也许更是起着决定性的作用。</p>
<p>最后关于居里夫人的标签,这本<a href="https://book.douban.com/subject/27002700/">北方文艺出版社</a>出版的封面了贴上了“两次获得诺贝尔奖第一人”、“爱因斯坦最推崇的女科学家”和“镭的发现者”三个稍微偏科学成就上的标签,而忽略她无私高尚的情操;另外我觉得她的伟大不需要通过伟大的爱因斯坦的认可来体现,她本身就很值得敬仰。</p>
<p>那么,相较于这些投身人类事业且人格高尚的科学家,我们这些庸庸碌碌、还不怎么努力的打工人存在的意义什么呢?为他们的研究打下坚实的物质基础?如果真是做出了点建设物质基础的贡献,好像还有那么点存在的意义。</p>tunzaotunzao@live.cn看到“自传”二字的时候我的第一反应就是这些名人都这么把自己当回事儿了吗?不过在第一章第一节中居里夫人就回答了我的疑惑:“刚开始的时候,我觉得这个建议(写自传)对我来说真的是难以接受”。后来想了下其实 Michelle Obama 的《Becoming》也是自传啊,当时阅读时就没有这么强烈的质疑,原因大概是我对书籍类型的区分上还停留在书名而不是书籍内容上。\[译\]Netflix数据平台自动诊断和自动修复系统2022-01-17T00:00:00+00:002022-01-17T00:00:00+00:00http://tunzao.me/translations/auto-diagnosis-and-remediation-in-netflix-data-platform<p>Netflix 目前拥有公有云上最复杂的数据平台之一,数据科学家和数据开发工程师在该平台上每天运行着大量批处理和流处理任务。随着我们的付费订阅用户在全球范围内的增长以及Netflix 正式进入游戏赛道,批、流处理任务的数量也迅速增加。我们的数据平台基于诸多分布式系统构建而成,由于分布式系统的固有特性,运行在数据平台上的任务不可避免地隔三差五出现问题。对这些问题进行定位分析是件很繁琐的事,涉及从多个不同系统收集日志和相关指标,并对其进行分析以找出问题的根本原因。在我们当前规模上,如果人工手动地定位和解决问题,即使是处理一小部分异常任务,也会给数据平台团队带来巨大的运维负担。另外,这种手动定位问题的方式,对平台用户工作效率上造成影响也不可小觑。</p>
<p>以上问题促使我们尽可能主动地检测和处理生产环境中的失败任务,尽量避免使团队工作效率降低的异常。于是我们在数据平台中设计、开发了一套名为 Pensive 的自动诊断和修复系统。目标是对执行失败和执行时间过长的任务进行故障诊断,并尽可能在无需人工干预的情况下对其进行修复。随着我们的平台不断发展,不同的场景和问题(scenarios and issues)都可能会造成任务失败,Pensive 必须主动在平台级别实时检测所有的问题,并诊断对相关任务的影响。</p>
<p>Pensive 由两个独立的系统组成,分别支持批任务和流任务的自动诊断。本文将简单介绍这两个系统的功能,以及它们是如何在离线平台(Big Data Platform)和实时计算平台(Real-time infrastructure)中执行自动诊断和修复。</p>
<h2 id="批任务自动诊断系统---batch-pensive">批任务自动诊断系统 - Batch Pensive</h2>
<p><img src="/images/netflix-batch-pensive.jpeg" alt="批任务自动诊断系统架构图" />
<em>图1: 批任务自动诊断系统架构图</em></p>
<p>数据平台中的批处理工作流任务使用Netfix 自研的<a href="https://netflixtechblog.com/scheduling-notebooks-348e6c14cfd6">调度服务</a> <a href="https://netflixtechblog.com/meson-workflow-orchestration-for-netflix-recommendations-fc932625c1d9">Meson</a> 调度执行,调度服务通过启动容器运行工作流节点,容器则运行在Netflix 自研的容器管理平台 <a href="https://netflixtechblog.com/titus-the-netflix-container-management-platform-is-now-open-source-f868c9fb5436">Titus</a> 上。这些工作流节点通过 <a href="https://netflix.github.io/genie/">Genie</a>(类似Apache Livy) 提交执行 Spark 和 TrinoDb 作业。 如果工作流节点失败,调度服务会向 Pensive 发送错误诊断请求。Pensive 从相关数据平台组件中收集该节点执行过程中产生的失败日志,然后提取分析异常堆栈。Pensive 依赖于基于正则表达式的规则引擎来进行异常诊断,这些规则是在不断的实践中总结归纳出来的。系统根据规则判定错误是由于平台问题还是用户Bug造成的、错误是否是临时抖动造成的(transient)。如果有个一条规则命中错误,Pensive 会将有关该错误的信息返回给调度服务。如果错误是临时抖动造成的,调度服务将使用指数退避策略(exponential backoff)重试执行该节点几次。</p>
<p>Pensive 最核心的部分是<strong>对错误进行分类的规则</strong>。随着平台的发展,这些规则需要不断被完善和改进,以确保 Pensive 维持较高的错误诊断率。最初,这些规则是由平台组件负责人和用户根据他们的经验或遇到的问题而贡献的。我们现在则采用了更系统的方法,将未知错误输入到机器学习系统,然后由机器学习任务对这些问题进行聚类,以针对常见错误归纳出新的正则表达式。我们将新正则提交给平台组件负责人,然后由相关负责人确认错误来源的分类以及它是否是临时抖动性的。未来,我们希望将这一过程自动化。</p>
<h3 id="检测平台级别的问题">检测平台级别的问题</h3>
<p>Pensive 可以对单个工作流节点的失败进行错误分类,但是通过使用 Kafka 和 Druid 对 Pensive 检测到的所有错误进行实时分析,我们可以快速识别影响所有工作流任务的平台问题。单个失败任务的诊断被存储在 Druid 表中后,我们的监控和警报系统 <a href="https://netflixtechblog.com/introducing-atlas-netflixs-primary-telemetry-platform-bd31f4d8ed9a">Atlas</a> 会对表中的数据每分钟进行一次聚合,并在因平台问题导致任务失败数量突增时发送告警。这大大减少了在检测硬件问题或新上线系统中的Bug所需的时间。</p>
<h2 id="流任务自动诊断系统---streaming-pensive">流任务自动诊断系统 - Streaming Pensive</h2>
<p><img src="/images/netflix-streaming-pensive.jpeg" alt="流任务自动诊断系统架构图" />
<em>图2: 批任务自动诊断系统架构图</em></p>
<p>Netflix 数据平台中的实时计算基于Apache Flink实现。大多数 Flink 任务运行在 <a href="https://netflixtechblog.com/keystone-real-time-stream-processing-platform-a3ee651812a">Keystone</a> 流任务处理平台上,该平台封装了底层 Flink 任务详细信息,给用户提供了消费 Kafka 消息和将处理结果写入到不同数据存储系统(如AWS S3 上的ElasticSearch 和 Iceberg)的能力。</p>
<p>由于数据平台管理着 Keystone 的数据处理流程,用户希望 Keystone 团队能够主动检测和修复平台问题,而无需他们的任何参与。此外,由于 Kafka 中的数据一般不会长期存储,这就需要我们及时地、在数据过期之前解决问题。</p>
<p>对于运行在 Keystone 上的每个 Flink 任务,我们会监控消费者的滞后程度(lag)指标。如果超过阈值,Atlas会向 Streaming Pensive 发送通知。</p>
<p>与批任务诊断系统一样,Streaming Pensive 也是基于规则引擎来诊断错误。但是,除了日志之外,Streaming Pensive 还会检查 Keystone 中多个组件的各种指标。错误可能出现在任何一个组件中,如源头的Kafka 、Flink 任务或者 Sink 端数据存储系统。Streaming Pensive 对这些日志和指标进行诊断,并尝试在问题发生时自动修复。我们目前能够自动修复的一些场景如下:</p>
<ul>
<li>如果 Streaming Pensive 发现一个或多个 Flink Task Manager 内存不足,系统可以增加 Task Manager 数重新部署 Flink 集群。</li>
<li>如果 Streaming Pensive 发现 Kafka 集群上的写入消息速率突然增加,它可以临时增加topic数据保留的大小和保留时间,这样我们就不会在消费者滞后时丢失任何数据。如果峰值在一段时间后消失,Streaming Pensive 可以自动恢复到之前的配置。否则,它将通知任务负责人,排查是否存在导致写入速度突增的错误,或者是否需要重新调整消费者以处理更高的写入速度。</li>
</ul>
<p>尽管我们的自动诊断成功率很高,但仍然存在无法实现自动化的情况。如果需要人工干预,Streaming Pensive 将通知相关组件团队,以便及时采取措施解决问题。</p>
<h2 id="未来规划">未来规划</h2>
<p>Pensive 对 Netflix 数据平台的日常运行至关重要。它帮助工程团队减轻运维负担,让他们专注于解决更关键和更具挑战性的问题。但我们的工作还远未完成,未来还有很多工作要做。以下是我们未来的规划:</p>
<ul>
<li>Batch Pensive 目前仅支持诊断失败的任务,我们希望后续扩展支持任务性能诊断以确定任务执行变慢的原因。</li>
<li>自动配置批处理工作流任务,以便它们能成功执行或快速且高效的执行。其中一个方向是 Spark 任务内存自动调优,是一项颇具挑战性的工作。</li>
<li>使用机器学习分类器来完善 Pensive(的规则)。</li>
<li>实时计算平台最近上线了Data Mesh功能,我们需要扩展 Streaming Pensive 来支持该功能的诊断。</li>
</ul>
<h2 id="致谢">致谢</h2>
<p>感谢 Netflix 数据平台的离线计算团队和实时计算团队的帮助和支持,如果没有他们这项工作是无法完成的。在我们致力于改善 Pensive 的过程中,他们一直是我们的坚实的支柱。</p>
<p><em>原文链接:</em> <a href="https://netflixtechblog.com/auto-diagnosis-and-remediation-in-netflix-data-platform-5bcc52d853d1">Auto-Diagnosis and Remediation in Netflix Data Platform</a></p>tunzaotunzao@live.cnNetflix 目前拥有公有云上最复杂的数据平台之一,数据科学家和数据开发工程师在该平台上每天运行着大量批处理和流处理任务。随着我们的付费订阅用户在全球范围内的增长以及Netflix 正式进入游戏赛道,批、流处理任务的数量也迅速增加。我们的数据平台基于诸多分布式系统构建而成,由于分布式系统的固有特性,运行在数据平台上的任务不可避免地隔三差五出现问题。对这些问题进行定位分析是件很繁琐的事,涉及从多个不同系统收集日志和相关指标,并对其进行分析以找出问题的根本原因。在我们当前规模上,如果人工手动地定位和解决问题,即使是处理一小部分异常任务,也会给数据平台团队带来巨大的运维负担。另外,这种手动定位问题的方式,对平台用户工作效率上造成影响也不可小觑。任务调度系统系列之Airflow2021-12-26T00:00:00+00:002021-12-26T00:00:00+00:00http://tunzao.me/articles/airflow<p>这是任务调度系统调研系列文章的开篇,后续会陆续调研Oozie,Azkaban,Dolphin Scheduler等系统。本文的主要内容是来自对官方文档和网上相关资料的调研,并非基于实际使用的经验总结,文章中难免会有一些不尽的细节或者关于Airflow错误的观点,如有不当之处,欢迎指正交流。</p>
<p>Airflow是一个基于Python开发的,通过编程的方式创建、调度和监控工作流(workflow)的平台。最早由Maxime Beauchemin于2014年10月在Airbnb创建,并且从创建之初就以开源的形式开发。2016年3月进入Apache基金孵化器,2019年1月正式成为Apache顶级项目。</p>
<p>官方文档中,特意强调了使用代码定义工作流的优点,使得工作流的维护、版本管理、测试和协作变得更加容易,直接复用代码开发过程中用到工具、系统就可以了,无需再重复造轮子,可以像开发软件系统一样开发数据任务,持续集成也是开箱即用。但是数据任务的测试向来不是一件简单的事情,不知道在实际使用中基于Airflow的数据开发CI/CD流畅度如何。这种基于Python代码定义工作流的方式使用门槛稍微高了一点。基于代码定义flow中节点的依赖关系,并不如通过界面拖拽那么直观,是不是也会使易用性大大折扣?</p>
<h2 id="架构">架构</h2>
<p><img src="/images/arch-diag-basic.png" alt="Airflow架构图" />
如上图所示,Airflow主要由以下几个部分组成:</p>
<h3 id="dag目录dag-directory">DAG目录(DAG Directory)</h3>
<p>存储定义DAG的Python文件的目录,调度器、执行器和执行节点会读取该目录下的文件获取DAG相关信息,所以要确保所有节点上DAG目录的数据同步。如何确保文件同步到也是一项复杂的工程。</p>
<h3 id="数据库metadata-database">数据库(Metadata Database)</h3>
<p>数据库主要用于存储系统的配置信息(系统变量,数据源链接信息,用户信息等)、解析DAG文件生成的DAG对象和任务执行的状态等信息。</p>
<h3 id="调度器scheduler">调度器(Scheduler)</h3>
<p>独立部署的进程,负责触发执行定时调度的工作流。调度器主要负责两件事:1)定时扫描DAG文件目录,解析变更或新增的DAG文件,并将解析后生成的DAG对象(Serialized DAG)存储到数据库;2)扫描数据库中的DAG对象,如果满足调度执行条件,则触发生成任务实例并提交给Executor执行。</p>
<h4 id="调度器高可用">调度器高可用</h4>
<p>从2.0开始,Airflow调度器支持高可用部署,采用了我之前实现调度服务高可用时使用的策略,通过数据库行锁的机制,实现多主的高可用。这样实现的好处是减少了leader选举、节点故障转移的复杂度。多个节点同时工作相较于主从模式也能获取较好的处理性能,可以通过横向扩展调度器提升调度服务的处理能力,但终究要受限于底层单点数据库的处理能力。如果执行事务的时长比较久,特别是事务中存在校验并发限制、资源使用配额的操作时,就很容易造成死锁,所以在Airflow实际部署中,高可用对数据库有着特殊的要求,需要数据库支持<code class="language-plaintext highlighter-rouge">SKIP LOCKED</code>或者<code class="language-plaintext highlighter-rouge">NOWAIT</code>。</p>
<h3 id="执行器executor">执行器(Executor)</h3>
<p>执行器负责执行或者提交执行任务实例(Task Instances)。执行器在实际部署中集成在调度器进程内,Airflow提供了两种类型的执行器,1)本地执行器,任务直接在调度器所在节点上执行;2)远程执行器,根据各执行器的实现方式,将任务提交到远程执行节点上执行。如果系统自带的执行器无法满足你的业务需求,可以自行实现自定义执行器。</p>
<p>系统自带本地执行器:</p>
<ul>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/executor/debug.html">Debug Executor</a>: 主要用于在IDE中对任务进行测试执行。</li>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/executor/local.html">Local Executor</a>:在调度器本地新建进程执行任务实例,可以通过<code class="language-plaintext highlighter-rouge">parallelism</code>参数控制最大任务并发数。</li>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/executor/sequential.html">Sequential Executor</a>: 可以理解为最大并发数1的Local Executor。</li>
</ul>
<p>系统自带远程执行器:</p>
<ul>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/executor/celery.html">Celery Executor</a>:<a href="https://docs.celeryproject.org/en/latest/getting-started/">Celery</a>是一个基于Python开发的分布式异步消息任务队列,通过它可以轻松的实现任务的异步处理。Celery Executor将任务发送到消息队列(RabbitMQ, Redis等),然后 Celery Worker 从消息队列中消费执行任务,并将执行结果写入到Celery的Backend中。Celery Executor 通过队列(queues)实现资源隔离,定义任务时指定使用的具体队列,则该任务只能由相应队列的worker执行。但是这个资源隔离的粒度有点粗,如果想实现更细粒度的资源,可以选择 Kubernetes Executor。</li>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/executor/kubernetes.html">Kubernetes Executor</a>: 通过K8S集群执行任务。Kubernetes Executor调用K8S API申请Worker Pod,然后由Pod负责任务的执行。</li>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/executor/celery_kubernetes.html">CeleryKubernetes Executor</a>:是上面两个执行器的组合,因为Airflow部署时只能指定一种类型执行器,如果既需要通过Celery执行又想提交到K8S集群执行,则可以选择该执行器。</li>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/executor/dask.html">Dask Executor</a>: <a href="http://distributed.dask.org/en/stable/">Dask</a>是基于Python实现的分布式计算框架,Dask Executor主要是通过Dask分布式集群执行任务。</li>
</ul>
<p>远程执行时所有执行节点都会直接数据库,鉴于弹性伸缩是Airflow的一大特性,如果执行节点规模太大对数据库造成的压力不可小觑,所以为什么要采用执行节点直连数据的方式呢?</p>
<h3 id="执行节点worker">执行节点(worker)</h3>
<p>负责具体任务的执行,根据执行器不同,可能是调度器所在节点,Celery Worker节点,K8S Pod等。</p>
<h3 id="webserver">WebServer</h3>
<p><img src="/images/airflow-ui.png" alt="WebServer" /></p>
<p>WebServer主要为用户提供了管理DAG(启用、禁用,手动执行),查看和操作DAG的执行状态,管理系统权限,查看和修改系统配置,管理数据源等功能。前文提到的通过代码定义依赖关系不直观的问题,Airflow在WebServer给了解决方案,<strong>运行DAG</strong>,然后通过WebServer的Graph视图以可视化的方式展示DAG。如果一定要在执行前可视化的方式查看DAG也可以在命令行执行<code class="language-plaintext highlighter-rouge">airflow dags show</code>生成Graph视图的图片。也许是我调研的还不够深入,难道就没有实时可视化展示DAG的方案?</p>
<h2 id="功能特性">功能特性</h2>
<h3 id="工作流定义">工作流定义</h3>
<p><img src="/images/airflow-dag.png" alt="DAG" /></p>
<p>Airflow通过Python代码以DAG的形式定义工作流,以下代码片段定义了上图由7个任务节点组成的DAG。</p>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="o">//</span> <span class="n">从2021年1月1日开始</span><span class="err">,</span><span class="n">每天零点调度</span>
<span class="k">with</span> <span class="n">DAG</span><span class="p">(</span>
<span class="s">"daily_dag"</span><span class="p">,</span> <span class="n">schedule_interval</span><span class="o">=</span><span class="s">"@daily"</span><span class="p">,</span> <span class="n">start_date</span><span class="o">=</span><span class="n">datetime</span><span class="p">(</span><span class="mi">2021</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">)</span> <span class="k">as</span> <span class="n">dag</span><span class="p">:</span>
<span class="n">ingest</span> <span class="o">=</span> <span class="n">DummyOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"ingest"</span><span class="p">)</span>
<span class="n">analyse</span> <span class="o">=</span> <span class="n">DummyOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"analyze"</span><span class="p">)</span>
<span class="n">check</span> <span class="o">=</span> <span class="n">DummyOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"check_integrity"</span><span class="p">)</span>
<span class="n">describe</span> <span class="o">=</span> <span class="n">DummyOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"describe_integrity"</span><span class="p">)</span>
<span class="n">error</span> <span class="o">=</span> <span class="n">DummyOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"email_error"</span><span class="p">)</span>
<span class="n">save</span> <span class="o">=</span> <span class="n">DummyOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"save"</span><span class="p">)</span>
<span class="n">report</span> <span class="o">=</span> <span class="n">DummyOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"report"</span><span class="p">)</span>
<span class="n">ingest</span> <span class="o">>></span> <span class="n">analyse</span> <span class="o">//</span> <span class="n">通过</span><span class="sb">`>>`</span><span class="p">,</span><span class="sb">`<<`</span><span class="n">定义节点依赖关系</span>
<span class="n">analyse</span><span class="p">.</span><span class="n">set_downstream</span><span class="p">(</span><span class="n">check</span><span class="p">)</span> <span class="o">//</span> <span class="n">通过</span><span class="sb">`set_downstream`</span><span class="p">,</span><span class="sb">`set_upstream`</span><span class="n">定义节点依赖关系</span>
<span class="n">check</span> <span class="o">>></span> <span class="n">Label</span><span class="p">(</span><span class="s">"No errors"</span><span class="p">)</span> <span class="o">>></span> <span class="n">save</span> <span class="o">>></span> <span class="n">report</span> <span class="o">//</span> <span class="n">通过</span><span class="sb">`Label`</span><span class="n">注释依赖关系</span>
<span class="n">check</span> <span class="o">>></span> <span class="n">Label</span><span class="p">(</span><span class="s">"Errors found"</span><span class="p">)</span> <span class="o">>></span> <span class="n">describe</span> <span class="o">>></span> <span class="n">error</span> <span class="o">>></span> <span class="n">report</span></code></pre></figure>
<p>DAG由节点、节点间的依赖关系以及节点间的数据流组成。节点的类型主要有以下三种:</p>
<ul>
<li>Operator: 任务节点,负责执行某种类型的任务。Airflow和社区已经实现了大量的<a href="https://airflow.apache.org/docs/apache-airflow/stable/concepts/operators.html">Operator</a>,基本覆盖了常用数据库,Hadoop生态活跃的系统和服务,以及AWS、Google和Azure三大海外云平台的系统和服务。</li>
<li>Sensor: 一种特殊的Operator,主要用来监听外部事件,可用作对外部系统、数据的依赖。Airflow通过<code class="language-plaintext highlighter-rouge">external_task</code> Sensor实现了DAG任务间的依赖。</li>
<li><code class="language-plaintext highlighter-rouge">@task</code>注解的Python函数,可以理解为基于Python装饰器定义的语法糖,能快速简洁的定义<code class="language-plaintext highlighter-rouge">PythonOperator</code>。</li>
</ul>
<p>如以上Python代码所示,节点间的依赖关系可以通过位操作符<code class="language-plaintext highlighter-rouge">>></code>/<code class="language-plaintext highlighter-rouge"><<</code>或<code class="language-plaintext highlighter-rouge">set_upstream</code>/<code class="language-plaintext highlighter-rouge">set_downstream</code>方法定义。</p>
<p>默认情况下下游节点要等上游所有节点执行成功后才开始执行,Airflow提供了多种方式来改变这一默认行为。第一种方式就是通过自定义节点的<a href="https://airflow.apache.org/docs/apache-airflow/stable/concepts/dags.html#trigger-rules">触发规则(Trigger Rules)</a>。Airflow提供了上游<code class="language-plaintext highlighter-rouge">所有节点都失败</code>、<code class="language-plaintext highlighter-rouge">所有节点执行完成</code>、<code class="language-plaintext highlighter-rouge">部分节点失败</code>、<code class="language-plaintext highlighter-rouge">部分节点成功</code>等多种规则,详情参考上述链接。另一种方式就是通过<code class="language-plaintext highlighter-rouge">控制节点</code>。目前有三种控制节点可以改变默认行为:</p>
<ul>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/concepts/dags.html#branching">分支节点(Branching)</a>: 通过<code class="language-plaintext highlighter-rouge">python_callable</code>函数返回的task_id决定执行下游哪个节点。</li>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/concepts/dags.html#latest-only">仅执行最新节点(Latest Only)</a>: 如果<code class="language-plaintext highlighter-rouge">仅执行最新节点</code>当前所属的DAG执行实例,不是改DAG最新的执行实例,则改节点及其所有子节点都不会被执行。</li>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/concepts/dags.html#depends-on-past">自依赖节点(Depends On Past)</a>: 依赖节点的上一次执行,只有上一次DAG调度中该节点执行成功了,才触发这一次执行。</li>
</ul>
<p>除了定义节点间的依赖关系,Airflow还通过<a href="https://airflow.apache.org/docs/apache-airflow/stable/concepts/xcoms.html">XComs(cross-communications)</a>实现了节点间的数据流。节点可以通过<code class="language-plaintext highlighter-rouge">xcom_push</code>方法输出数据,其他节点可以通过<code class="language-plaintext highlighter-rouge">xcom_pull</code>方法获取节点的输出数据。</p>
<h3 id="调度">调度</h3>
<p>定时调度是通过DAG的<code class="language-plaintext highlighter-rouge">schedule_interval</code>参数定义,传参可以是<code class="language-plaintext highlighter-rouge">datetime.timedelta</code>对象、cron表达式(Unix格式)或者<code class="language-plaintext highlighter-rouge">@daily</code>、<code class="language-plaintext highlighter-rouge">@monthly</code>等预设cron表达式。对于任务调度时间的定义,Airflow采用了目前我所接触到的任务调度系统中不同的视角,用数据时间(Airflow称它为logical date,有的系统称它为etl_date)来定义任务调度时间。举个例子,假如DAG配置每天调度一次,在Airflow中<code class="language-plaintext highlighter-rouge">2021-12-26</code>这次的调度实例,要在<code class="language-plaintext highlighter-rouge">2021-12-27</code>这天凌晨才会生成,处理的是<code class="language-plaintext highlighter-rouge">2021-12-26</code>的数据。而在其他系统中<code class="language-plaintext highlighter-rouge">2021-12-26</code>这次的调度实例就是在<code class="language-plaintext highlighter-rouge">2021-12-26</code>生成,处理的是<code class="language-plaintext highlighter-rouge">2021-12-25</code>的数据。</p>
<p>针对MissFire策略(概念来自quartz),Airflow提供了<code class="language-plaintext highlighter-rouge">catchup</code>参数。如果<code class="language-plaintext highlighter-rouge">catchup</code>设置为<code class="language-plaintext highlighter-rouge">false</code>,则未生成的调度时间段直接跳过,只生成最新的调度实例。另外在禁用和启用调度DAG后<code class="language-plaintext highlighter-rouge">catchup</code>逻辑也会触发。</p>
<h4 id="超时失败和报警">超时失败和报警</h4>
<p>如果要限制节点最大执行时间,可以设置<code class="language-plaintext highlighter-rouge">execution_timeout</code>参数,节点在<code class="language-plaintext highlighter-rouge">execution_timeout</code>配置时间内未执行成功则自动超时失败。任务执行超时报警是通过<code class="language-plaintext highlighter-rouge">sla</code>参数配置的,节点在<code class="language-plaintext highlighter-rouge">sla</code>指定的时间内没有执行成功,系统自动发送SLA未满足邮件,也可以通过<code class="language-plaintext highlighter-rouge">sla_miss_callback</code>回调函数,自定义任务超时的逻辑。</p>
<p>关于报警,Airflow提供了<code class="language-plaintext highlighter-rouge">email_on_failure</code>,<code class="language-plaintext highlighter-rouge">email_on_retry</code>参数控制节点在执行失败、重试时是否发送邮件报警。在实际生产环境中,邮件报警肯定是不能满足需求的,其他报警方式可以通过自定义<code class="language-plaintext highlighter-rouge">on_failure_callback</code>,<code class="language-plaintext highlighter-rouge">on_retry_callback</code>回调函数实现。</p>
<h4 id="并发限制">并发限制</h4>
<p>Airflow提供了多种粒度的并发限制。</p>
<h5 id="系统级别">系统级别</h5>
<ul>
<li><code class="language-plaintext highlighter-rouge">parallelism</code>: Airflow并发执行的任务数</li>
<li><code class="language-plaintext highlighter-rouge">max_active_runs_per_dag</code>: 每个DAG可并发生成的DAG调度实例数</li>
<li><code class="language-plaintext highlighter-rouge">dag_concurrency</code>: 每个DAG实例并发执行的任务数</li>
<li><code class="language-plaintext highlighter-rouge">worker_concurrency</code>: 每个执行节点可并发执行的任务数,仅 Celery Executor 的执行节点支持该配置</li>
</ul>
<h5 id="dag级别">DAG级别</h5>
<ul>
<li><code class="language-plaintext highlighter-rouge">max_active_runs</code>: 当前DAG可并发生成的DAG调度实例数,该配置会覆盖系统级别的<code class="language-plaintext highlighter-rouge">max_active_runs_per_dag</code></li>
<li><code class="language-plaintext highlighter-rouge">concurrency</code>: 当前DAG实例并发执行的任务数,该配置会覆盖系统级别的<code class="language-plaintext highlighter-rouge">dag_concurrency</code></li>
</ul>
<h5 id="任务级别">任务级别</h5>
<ul>
<li><code class="language-plaintext highlighter-rouge">pool</code>:pool是Airflow用于实现跨DAG、跨任务的并发限制方案。定义任务时指定任务所属pool、任务使用的slot数、任务优先级;pool资源使用达到上限后,所有隶属该pool的任务实例进入排队状态,有空闲资源释放时,高优先任务优先获取资源。资源分配的具体策略这里就没有在深入研究。</li>
<li><code class="language-plaintext highlighter-rouge">task_concurrency</code>:当前任务节点的最大并发执行实例个数,类似于<code class="language-plaintext highlighter-rouge">max_active_runs</code>,只是粒度更细</li>
</ul>
<h3 id="补数据和手动执行">补数据和手动执行</h3>
<p>补数据(backfill)可以通过以下命令触发:</p>
<figure class="highlight"><pre><code class="language-sh" data-lang="sh">airflow dags backfill <span class="nt">--start-date</span> START_DATE <span class="nt">--end-date</span> END_DATE dag_id</code></pre></figure>
<p>默认情况下,补数据只生成并执行指定时间范围内缺失的调度记录。再举个例子,DAG每天调度一次,现在要补2021-12-01到2021-12-03之间的数据,其中2021-12-02这天已经调度执行过,则补数据任务只会创建执行2021-12-01和20210-12-03的调度记录。backfill命令提供了多种选项来覆盖这一默认策略。</p>
<p>手动执行可以通过命令<code class="language-plaintext highlighter-rouge">airflow dags trigger --exec-date logical_date run_id</code>或者通过WebServer触发。</p>
<h3 id="数据血缘">数据血缘</h3>
<p>本着让专业的人干专业的事的理念,Airflow依托于第三方元数据管理系统实现数据血缘管理,平台本身只实现血缘的搜集和上报。通过任务的<code class="language-plaintext highlighter-rouge">inlets</code>和<code class="language-plaintext highlighter-rouge">outlets</code>属性定义任务的血缘信息,血缘信息在任务的<code class="language-plaintext highlighter-rouge">post_execute</code>方法中推送到XCOM,然后再由<code class="language-plaintext highlighter-rouge">LineageBackend</code>把血缘信息写到Atlas、DataHub(WhereHows)或者自定义的元数据管理系统。</p>
<h2 id="总结">总结</h2>
<p>本文通过官方文档和网上相关资料,“纸上”静态地调研了Airflow的系统架构和功能特性。整体而言,Airflow是一个调度功能完善、扩展伸缩性良好、文档详尽、社区强大活跃的工作流调度平台。个人感觉在任务调度系统选型上,可能阻碍Airflow入选的最主要因素是基于Python技术栈实现的整个系统和DAG定义。如果负责平台的同学和系统面向的用户有Python相关技术背景,从纸面上看,Airflow是个非常不错、甚至是第一优先级的选择。</p>
<p><em>参考资料</em></p>
<p><a href="https://airflow.apache.org/docs/apache-airflow/stable/index.html">Apache Airflow Documentation</a><br />
<a href="https://www.astronomer.io/blog/airflow-2-scheduler">The Airflow 2.0 Scheduler</a><br />
<a href="https://www.astronomer.io/guides/airflow-scaling-workers">Scaling Out Airflow</a><br />
<a href="https://www.astronomer.io/guides/airflow-ui">The Airflow UI</a></p>tunzaotunzao@live.cn这是任务调度系统调研系列文章的开篇,后续会陆续调研Oozie,Azkaban,Dolphin Scheduler等系统。本文的主要内容是来自对官方文档和网上相关资料的调研,并非基于实际使用的经验总结,文章中难免会有一些不尽的细节或者关于Airflow错误的观点,如有不当之处,欢迎指正交流。Kubernetes原理(1)2020-04-06T00:00:00+00:002020-04-06T00:00:00+00:00http://tunzao.me/articles/how-k8s-works-part1<p>本文将主要介绍Kubernetes集群中各个组件的工作原理(非源码级别),以帮助大家更深入的了解K8S,从而更高效地使用K8S。由于本篇不是入门级文章,需要大家对K8S有些基本的了解,知道它提供了哪些功能,才能更好地理解这篇文章。如果还不了解K8S的功能特性,可以参考这篇<a href="https://zhuanlan.zhihu.com/p/99397148">一起学习k8s</a>做个快速入门。</p>
<h2 id="架构">架构</h2>
<p><img src="/images/k8s-architecture.png" alt="K8S架构图" /></p>
<p>作为资源管理和调度系统,K8S集群的整体架构跟yarn或mesos都有着异曲同工之处。K8S集群主要有两种角色的节点组成:主节点(Control Plane)和工作节点。主节点主要负责存储、管理集群的状态,同时向客户端提供访问集群的接口;工作节点则主要负责执行、监控客户端通过主节点提交的容器化应用。</p>
<h2 id="主节点">主节点</h2>
<p>主节点在K8S集群里只是个逻辑上的概念,实际部署中它是由多个独立部署、各司其职的组件共同组成,这些组件也并非一定要部署在同一台机器上。主节点主要由以下组件组成:</p>
<ul>
<li>etcd</li>
<li>API Server</li>
<li>调度器(Scheduler)</li>
<li>控制器(Controller Mananger)</li>
</ul>
<h3 id="etcd">etcd</h3>
<p><a href="https://etcd.io/">etcd</a>是基于<a href="https://raft.github.io/">raft</a>一致性算法实现的高可用键值存储系统,主要用于共享配置和服务发现,类似于Hadoop生态圈的Zookeeper。etcd在K8S集群的作用是持久化集群的状态和元数据信息。通过K8S接口创建的各种资源信息(Pods, ReplicationControllers, Services等)及其状态都存储在etcd。资源在etcd的存储结构大概是这个样子:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/registry/configmaps
/registry/daemonsets
/registry/deployments
/registry/events
/registry/namespaces
/registry/pods
...
</code></pre></div></div>
<p>K8S集群的所有组件中,唯一跟etcd交互的组件是API Server,其他组件或者客户端需要查询、变更集群状态时都需要通过调用或者监听API Server提供的接口。这样设计的好处是:1)统一变更集群状态的入口,在入口处做统一的鉴权和校验,从而保证系统状态的一致性;2)封装底层存储的细节,如果哪天心血来潮想换个存储方案,其他组件和客户端则不需要做任何调整。</p>
<h3 id="api-server">API Server</h3>
<p>如上文提到的那样,API Server是其他组件和客户端跟K8S集群交互的统一入口。它对外提供了用于查询和变更集群状态的RESTful API。这些API会对客户端请求进行鉴权并校验提交资源合法性,以保证系统安全和集群状态的稳定性。鉴权的方式和校验资源合法性的规则都可以通过插件的形式自行扩展。API Server 同时采用了乐观锁来保证在并发更新情况下资源状态的一致性。</p>
<p>下文将要提到的调度器和资源管理器都需及时感知到集群系统状态的变更然后做出相应的响应,如果通过轮询的方式定时查询集群状态势必会给API Server造成巨大的压力。API Server提供了一种监听机制(watch),客户端可以通过HTTP接口监听自己关注的资源变更事件,一旦有相关变更API Server就会向客户端发送相应的事件信息。</p>
<p><img src="/images/k8s-watch.png" alt="Api server watch" /></p>
<blockquote>
<p>监听机制理论上应该基于TCP长连接实现才最有效,虽然HTTP协议可以通过<code class="language-plaintext highlighter-rouge">Connection keep-alive</code>实现多个HTTP请求复用同一个TCP连接,但针对单次HTTP请求来说还是一次短连接,那么K8S是如何通过HTTP实现监听的呢?可以参考下<a href="https://zhuanlan.zhihu.com/p/59660536">这篇文章</a></p>
</blockquote>
<p>统一集群交互入口的优点在介绍etcd时提到了,但是这种类似于服务总线的中心化设计使得API Server成为整个系统的单点,一旦出现故障会导致整个集群都不可用。好在API Server是无状态的,支持灵活地横向扩展,持久化集群状态的etcd也支持分布式部署,但是由于etcd并不支持分片,这可能是限制K8S集群规模的制约点。</p>
<h3 id="调度器">调度器</h3>
<p>调度器的工作其实很简单,通过API Server监听所有未分配节点的Pod,然后给这个Pod<strong>分配一个合适的工作节点</strong>,并把分配到节点写入到Pod的配置信息里。余下的容器创建和监控工作就由分配到的节点来完成了。</p>
<p>不过选择节点过程并不像一句“分配一个合适的节点”描述的那么简单,默认调度器选择节点大致分为两个步骤:首先筛选出所有满足Pod需求的节点,然后再在这些节点中选择一个最优的节点,如果有多个最优,则通过轮询(Round-robin)的方式在其中选择一个。筛选节点的条件如下:</p>
<ul>
<li>是否满足Pod的硬件资源要求(内存、CPU等)</li>
<li>是否有剩余资源</li>
<li>如果Pod指定了节点,当前节点是否是指定节点</li>
<li>节点标签是否满足Pod的node selector</li>
<li>Pod要绑定的端口在该节点上是否被占用</li>
<li>Pod指定挂载的卷能否在节点上挂载</li>
<li>Pod是否能容忍节点的污点(taints)</li>
<li>Pod调度到该节点上是否满足pod的亲和性与非亲和性(affinity or anti-affinity)要求</li>
</ul>
<p>调度器很难通过一套普适算法满足所有的需求,所以K8S支持部署多个调度器,可以在Pod通过设置<code class="language-plaintext highlighter-rouge">schedulerName</code>指定需要使用的调度器,如果不指定则使用默认调度器。如果系统提供的调度器不能满足需要,可以自行实现并部署到集群。</p>
<h3 id="控制器">控制器</h3>
<p>控制器由多个相互独立Controller组成,每个Controller负责管理跟它绑定的资源,确保它所管理的资源维持在既定状态。这个描述可能有点抽象,通过下面对各Controller功能介绍能更直观地了解它们的作用。</p>
<h4 id="replication-manager">REPLICATION MANAGER</h4>
<p>Replication Manager负责管理资源<code class="language-plaintext highlighter-rouge">ReplicationController</code>,确保运行中的Pod副本数满足<code class="language-plaintext highlighter-rouge">ReplicationController</code>的定义。它通过API Server监听<code class="language-plaintext highlighter-rouge">ReplicationController</code>及其管理的Pod信息,如果Pod副本数不满足,则调用接口新建或删除Pod,其他controller的工作原理也都类似。新建Pod时Replication Manager并不直接运行Pod,而是像其他客户端一样往API Server提交一条创建Pod请求,然后由Scheduler给该Pod分配节点,再由工作节点启动相应的container。</p>
<h4 id="replicasetdaemonsetjob-controller">REPLICASET,DAEMONSET,JOB CONTROLLER</h4>
<p>分别负责管理<code class="language-plaintext highlighter-rouge">ReplicaSet</code>,<code class="language-plaintext highlighter-rouge">DaemonSet</code>和<code class="language-plaintext highlighter-rouge">Job</code>资源,监听相应资源信息,根据资源里Pod模板创建Pod。</p>
<h4 id="deployment-controller">DEPLOYMENT CONTROLLER</h4>
<p>负责管理<code class="language-plaintext highlighter-rouge">Deployment</code>资源。每次<code class="language-plaintext highlighter-rouge">Deployment</code>被修改时,Deployment Controller都会创建一个新的<code class="language-plaintext highlighter-rouge">ReplicaSet</code>版本,然后根据Deployment中定义的策略,逐渐调整新老<code class="language-plaintext highlighter-rouge">ReplicaSet</code>的副本数,直到新版本完全替换掉老版本。</p>
<h4 id="statefulset-controller">STATEFULSET CONTROLLER</h4>
<p>作用跟REPLICASET CONTROLLER类似,根据<code class="language-plaintext highlighter-rouge">StatefulSet</code>的配置调整Pod,不同的是它还负责初始化和管理Pod实例的PVC(PersistentVolumeClaims)</p>
<h4 id="node-controller">NODE CONTROLLER</h4>
<p>负责维护集群的节点列表信息,监控节点的健康状况,将Pod从不可用节点上删除。</p>
<h4 id="service-和-endpoints-controller">SERVICE 和 ENDPOINTS CONTROLLER</h4>
<p>Service controller 监听<code class="language-plaintext highlighter-rouge">Service</code>资源的变更,创建或删除相应的<code class="language-plaintext highlighter-rouge">Service</code>。Endpoints controller 则同时监听Services和Pod资源信息,根据相应的变更更新Endpoints(IP+端口),并在Service被删除时删除对应的Endpoints。</p>
<h4 id="namespace-controller">NAMESPACE CONTROLLER</h4>
<p>Namespace Controller功能相对简单,负责在namespace被删除时,删除该namespace下所有的资源。</p>
<h4 id="persistentvolume-controller">PERSISTENTVOLUME CONTROLLER</h4>
<p>PersistentVolume Controller主要负责给PVC绑定/解绑PersistentVolume。</p>
<h4 id="controller间的协作">Controller间的协作</h4>
<p>Controller彼此之间没有直接依赖,甚至感知不到彼此的存在,但是它们管理的资源是存在依赖关系的,这种依赖关系通过总线API Server在Controller间传递,以此实现Controller间的协作。以<code class="language-plaintext highlighter-rouge">Deployment</code>为例,它依赖于<code class="language-plaintext highlighter-rouge">ReplicaSet</code>创建Pod。当客户端向API Server提交<code class="language-plaintext highlighter-rouge">Deployment</code>时,Deployment Controller根据配置创建<code class="language-plaintext highlighter-rouge">ReplicaSet</code>,Replica Controller监听到<code class="language-plaintext highlighter-rouge">ReplicaSe</code>t事件后创建Pod,然后由Scheduler调度Pod到指定节点,再由Kubelet启动执行Pod。</p>
<p><img src="/images/k8s-controller-corp.png" alt="Controller间的协作" /></p>
<h3 id="调度器和控制器的高可用">调度器和控制器的高可用</h3>
<p>调度器和控制器的执行逻辑是典型的checkAndSet,如果check和set操作不能原子性地完成(监听API Server事件然后做出响应不能原子性地完成),在并发情况下会造成数据不一致,这就限制了调度器和控制器的横向扩展。为了保证调度器和监控器的高可用,K8S默认采用热备份的方式部署多个调度器和控制器实例,通过leader选举的方式选举master节点,由master节点来处理调度和控制管理工作,其他实例在master故障后竞争成为master来接管调度工作。</p>
<p>K8S通过更新<code class="language-plaintext highlighter-rouge">Endpoints</code>或者<code class="language-plaintext highlighter-rouge">ConfigMaps</code>资源实现leader选举,成功更新该资源的客户端获取leader角色,并定时上报心跳以维持自己的leader角色,如果leader失联或者心跳超时,其它客户端开始发起更新请求竞争成为leader角色。考虑到K8S应用也有像调度器和控制器这样无法很容易实现横向扩展但又需要保持高可用的需求,K8S集群提供了开箱即用的leader选举功能,具体使用和细节可以参考<a href="https://github.com/kubernetes-retired/contrib/tree/master/election">这篇文章</a>。</p>
<h2 id="工作节点">工作节点</h2>
<p>工作节点的组成相对简单,由Kubelet和Kubernetes Service Proxy组成,并且这两个组件需要同时部署在同一节点上。</p>
<h3 id="kubelet">Kubelet</h3>
<p>Kubelet主要负责执行、监控由调度器分配的Pod。Kubelet启动后会向API Server注册一个Node资源,并定时上报节点的信息。随后通过监听API Server获取分配到当前节点的Pod,拉取Pod使用的镜像并启动容器,然后定时上报容器的运行状态、事件和资源使用信息到API Server。</p>
<p>Kubelet在启动Pod中定义的容器同时还会启动一个特殊的pause容器(该容器的状态一直是Pause),又叫Infra容器。它的作用是为了保证Pod中所有的容器都共享同一个namespace(比如共用同一个IP地址)。在创建其他容器前,Kubelet先创建Pause容器,然后再创建其他容器并加入Pause容器的namespace,在容器发生重启时同样会再次加入Pause容器的namespace。</p>
<p>Kubelet除了通过监听API Server获取Pod任务外,还可以通过监听HTTP接口或者本地路径获取Pod并执行、更新之。通过这种方式可以实现由Kubelet运行和管理主节点的所有组件,在一定程度实现编程语言里的自举。</p>
<h3 id="kubernetes-service-proxykube-proxy">Kubernetes Service Proxy(kube-proxy)</h3>
<p>kube-proxy的作用是当客户端调用Service的虚IP和端口访问Service时,确保该请求可以被成功地转发到Service管理的Pod中去,如果Service由多个Pod组成,kube-proxy还要保证在Pod间做好负载。K8S集群网络问题相对比较复杂,打算单独放在一篇文章里介绍,敬请期待。</p>
<p>参考</p>
<ul>
<li>Kubernetes in Action</li>
<li><a href="https://jimmysong.io/kubernetes-handbook/concepts/pause-container.html">Pause容器</a></li>
</ul>tunzaotunzao@live.cn本文将主要介绍Kubernetes集群中各个组件的工作原理(非源码级别),以帮助大家更深入的了解K8S,从而更高效地使用K8S。由于本篇不是入门级文章,需要大家对K8S有些基本的了解,知道它提供了哪些功能,才能更好地理解这篇文章。如果还不了解K8S的功能特性,可以参考这篇一起学习k8s做个快速入门。Java的那些日志框架们2020-01-04T00:00:00+00:002020-01-04T00:00:00+00:00http://tunzao.me/articles/java-logging<p>日志在排查线上问题、跟踪线上系统运行情况中发挥着重要作用。在Java应用的开发中,常见的日志框架有<a href="https://commons.apache.org/proper/commons-logging/">JCL</a>(commons-logging),<a href="http://www.slf4j.org/">slf4j</a>,<a href="https://docs.oracle.com/javase/8/docs/api/java/util/logging/Logger.html">JUL</a>(java.util.logging),<a href="http://logging.apache.org/log4j/1.2/">log4j</a>,<a href="https://logging.apache.org/log4j/2.x/">log4j2</a>,<a href="http://logback.qos.ch/">logback</a>等。这些日志框架大致可以分为两类,一类是日志门面(JCL、slf4j),定义日志的抽象接口;另一类是日志实现(JUL,log4j,log4j2,logback),负责真正地处理日志。为什么会有这么多的日志框架呢?从Java日志框架的发展史里大概可以一探究竟。</p>
<blockquote>
<p>Java日志框架的发展历史</p>
<ul>
<li>log4j是Java社区最早的日志框架,推出后一度成为Java的事实日志标准,据说Apache曾建议Sun把log4j加入到Java标准库中,但是被Sun拒绝</li>
<li>在Java1.4中,Sun在标准库中推出了自己的日志框架java.util.logging,功能相对简陋</li>
<li>虽然JUL相对简陋,但还是有类库采用了它,这就出现了同一个项目中同时使用log4j和JUL要维护两套配置的问题,Apache试图解决这个问题,推出了JCL日志门面(接口),定义了一套日志接口,底层实现支持log4j和JUL,但是并没有解决多套配置的问题</li>
<li>log4j的主力开发Ceki Gülcü由于某些原因离开了Apache,创建了slf4j日志门面(接口),并实现了性能比log4j性能更好的logback(如果Ceki Gülcü没有离开Apache,这应该就是log4j2的codebase了)</li>
<li>Apache不甘示弱,成立了不兼容log4j 1.x的log4j2项目,引入了logback的特性(还酸酸地说解决了logback架构上存在的问题),但目前采用率不是很高</li>
</ul>
</blockquote>
<h2 id="日志框架选择">日志框架选择</h2>
<p>那么面对这些日志框架,该如何选择呢?如果你是在开发一个新的项目(类库)而不是维护一个上古的遗留代码,那么在打印日志时推荐使用日志门面,秉承面向接口编程的思想,与具体的日志实现框架解耦,这样日后可以很容易地切换到其他的日志实现框架。</p>
<p>特别是当你的代码以SDK的方式提供给别人使用时,使用日志门面能避免使用方可能出现的日志框架冲突问题。如果你的SDK里使用了log4j,而使用方的应用里使用的logback,这时使用方就不得不分别针对log4j和logback维护两套日志配置文件,来确保所有日志正常的输出(slf4j提供了冲突解决方案,稍后在下文介绍)。</p>
<p>在目前已有的两个日志门面框架中,slf4j规避了JCL在部分场景下因为ClassLoader导致绑定日志实现框架失败的问题;能支持以上提到的所有日志实现框架;且slf4j支持占位符功能,在需要拼接日志的情况在接口层面就比JCL有更好的性能,所以推荐使用slf4j,下面简单多介绍下slf4j。</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="c1">// slf4j的占位符功能</span>
<span class="no">LOGGER</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"hello {}"</span><span class="o">,</span> <span class="n">name</span><span class="o">);</span></code></pre></figure>
<h2 id="slf4j如何实现对log4j和jul的支持">slf4j如何实现对log4j和JUL的支持</h2>
<p>logback因为本身就实现了slf4j-api,所以天然就能很好地支持slf4j,但是log4j和JCL不同,早在slf4j之前就已经存在,他们可不是为了实现slf4j而设计的,那么如何实现slf4j和他们的绑定呢?答案就是适配器模式。如下图,slf4j分别为log4j和JCL实现了适配层<code class="language-plaintext highlighter-rouge">slf4-log4j12.jar</code>和<code class="language-plaintext highlighter-rouge">slf4j-jdk14.jar</code>,通过适配层把日志的处理转发给底层日志实现框架。</p>
<p><img src="/images/slf4j-concrete-bindings.png" alt="concrete-bindings" /></p>
<p>下面是使用log4j 1.x做为日志实现框架的maven依赖配置:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.slf4j<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>slf4j-api<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.7.26<span class="nt"></version></span>
<span class="nt"></dependency></span>
<span class="c"><!-- slf4j-log4j12 依赖了log4j,不需要再显示地依赖log4j --></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.slf4j<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>slf4j-log4j12<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.7.26<span class="nt"></version></span>
<span class="nt"></dependency></span></code></pre></figure>
<h2 id="slf4j解决日志实现框架冲突">slf4j解决日志实现框架冲突</h2>
<p>由于历史原因,总会遇到依赖的多个类库使用不同日志实现框架的情况,之前也提到了,为了确保日志正常输出,需要针对多个的日志实现框架维护多个配置文件。为了解决这个问题,slf4j再次基于适配器模式提供了解决方案,针对不同的日志实现框架实现了xxx-over-slf4j适配层,把对日志实现框架的调用转发到slf4j-api,再由slf4j把日志处理转发给日志实现框架。</p>
<p><img src="/images/slf4j-legacy.png" alt="日志依赖冲突" /></p>
<p>上图分别展示了把log4j,logback,JUL,JCL的调用分别转换成其中一种日志实现框架的示意图。假设项目依赖的SDK分别使用了log4j、JUL和JCL,打算把日志实现框架统一成log4j,maven依赖配置如下:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="nt"><dependency></span>
<span class="nt"><groupId></span>me.tunzao<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>classloader-common<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.0-SNAPSHOT<span class="nt"></version></span>
<span class="nt"><exclusions></span>
<span class="c"><!-- 排除对jcl的依赖 --></span>
<span class="nt"><exclusion></span>
<span class="nt"><artifactId></span>commons-logging<span class="nt"></artifactId></span>
<span class="nt"><groupId></span>commons-logging<span class="nt"></groupId></span>
<span class="nt"></exclusion></span>
<span class="nt"></exclusions></span>
<span class="nt"></dependency></span>
<span class="c"><!-- 把对jcl的请求转发给slf4j --></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.slf4j<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>jcl-over-slf4j<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.7.26<span class="nt"></version></span>
<span class="nt"></dependency></span>
<span class="c"><!-- 把对jul的请求转发给slf4j --></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.slf4j<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>jul-to-slf4j<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.7.26<span class="nt"></version></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.slf4j<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>slf4j-api<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.7.26<span class="nt"></version></span>
<span class="nt"></dependency></span>
<span class="c"><!-- slf4j-log4j12 依赖了log4j,不需要再显示地依赖log4j --></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.slf4j<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>slf4j-log4j12<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.7.26<span class="nt"></version></span>
<span class="nt"></dependency></span></code></pre></figure>
<p>需要注意的是log4j-over-slf4j.jar 和 slf4j-log4j12.jar 不能同时出现在classpath下,否则就会因为循环调用而堆栈溢出,同理jul-to-slf4j.jar和slf4j-jdk14.jar、jcl-over-slf4j.jar和slf4j-jcl.jar亦不能同时出现。</p>
<p>参考</p>
<p><a href="http://www.slf4j.org/manual.html">SLF4J user manual</a></p>
<p><a href="http://www.slf4j.org/legacy.html">Bridging legacy APIs</a></p>
<p><a href="https://blog.frankel.ch/thoughts-on-java-logging-and-slf4j/">Thoughts on Java logging and SLF4J</a></p>
<p><a href="https://stackify.com/logging-java/">The State of Logging in Java</a></p>
<p><a href="https://blog.csdn.net/xintonghanchuang/article/details/90752323">Java系统中常用日志框架</a></p>tunzaotunzao@live.cn日志在排查线上问题、跟踪线上系统运行情况中发挥着重要作用。在Java应用的开发中,常见的日志框架有JCL(commons-logging),slf4j,JUL(java.util.logging),log4j,log4j2,logback等。这些日志框架大致可以分为两类,一类是日志门面(JCL、slf4j),定义日志的抽象接口;另一类是日志实现(JUL,log4j,log4j2,logback),负责真正地处理日志。为什么会有这么多的日志框架呢?从Java日志框架的发展史里大概可以一探究竟。长大后我就成了你2019-08-15T00:00:00+00:002019-08-15T00:00:00+00:00http://tunzao.me/articles/chrome<p>第一次接触Chrome是在大学寝室里龙哥的电脑上,那时候IE6还霸占者绝对的浏览器市场份额,装着IE内核、披着华丽外衣的国产浏览器也正春笋般崛起,真是有点被那蓝色简洁的窗体给惊讶到了,但是并没有让我从Firefox转战到Chrome,这么简单的一款产品在功能性和扩展性上怎么能和插件丰富的Firefox相比,于是我坚守在Firefox的阵地,同时鄙视、谩骂着IE。</p>
<p>后来Chrome支持了扩展功能、有了自己的Web Store,而且没有Firefox向后兼容的历史包袱,交互和体验上都做的比Firefox要好,真正让我忍受不了Firefox而转向Chrome的原因竟是vim插件,由于Firefox的限制vimperator插件不支持<code class="language-plaintext highlighter-rouge">Ctrl+[</code>,而且<code class="language-plaintext highlighter-rouge">f</code>键给链接贴上的数字按钮巨丑,而Chrome的<code class="language-plaintext highlighter-rouge">Vimium</code>在这两方面都比Firefox做的要好,于是狠心抛弃了Firefox。</p>
<p>在Firefox Quantum发布后不久,<code class="language-plaintext highlighter-rouge">Vimium</code>插件被成功的移植到Firefox,我又换回了Firefox。vim插件不是再次转换阵营的主要原因,更多是被Firefox的关于页给“洗脑”,为了保护隐私,为了自由, you name it。</p>
<blockquote>
<p>Firefox is designed by <a href="https://www.mozilla.org">Mozilla</a>, a <a href="https://www.mozilla.org/credits/">global community</a> working to keep the Web open, public and accessible to all.</p>
</blockquote>
<p>十年后的如今Chrome占据着浏览器市场的绝对份额,也有些网站从只兼容IE变成了只兼容Chrome。独占鳌头的Chrome会慢慢地自定义web标准吗?会成为下一个IE吗?</p>tunzaotunzao@live.cn第一次接触Chrome是在大学寝室里龙哥的电脑上,那时候IE6还霸占者绝对的浏览器市场份额,装着IE内核、披着华丽外衣的国产浏览器也正春笋般崛起,真是有点被那蓝色简洁的窗体给惊讶到了,但是并没有让我从Firefox转战到Chrome,这么简单的一款产品在功能性和扩展性上怎么能和插件丰富的Firefox相比,于是我坚守在Firefox的阵地,同时鄙视、谩骂着IE。读《大教堂与集市》2018-10-13T00:00:00+00:002018-10-13T00:00:00+00:00http://tunzao.me/articles/thoughts-on-the-cathedral-and-the-bazaar<p>浏览开发者头条的时候看到一篇题为《系统架构》读书笔记的文章,突然想到自己刚读完《大教堂与集市》,还没有留下一点点观后感之类的文字,没有对书本的思考,这本书就白读了吧,就像之前的《黑客与画家》,现在都记不清它都有些什么内容了。所以决定还是写点读后感类的内容。</p>
<p>忘了是从哪天开始了,网易云阅读也没有阅读统计的功能(就不能把数据开放出来给用户看吗?),大概持续了一个月吧,用零碎时间和过节回家的时间看完了这本开源世界的圣经。没有参与过开源项目,更算不上是黑客,可能连基本的程序员都还不能胜任。也没有接触过真的的黑客(期望着能和牛人一起共事),自然不太能真实地了解到黑客是怎么的存在,而且从接触的小圈子里看也没有特别热衷开源的同事,更不了解中国的开源现状如何,是不是也有一些狂热的开源分子呢?只是依稀的知道开源中国一直在促进开源相关的事,具体细节就知之甚少了。</p>
<p>书本的内容依然模糊了,不过学习了一个新词:同侪。内容主要围绕着开源在展开,从计算机上古时代开始,介绍开源的发展历史,后来从经济学、社会学等方面分析了为什么开源能取得成功以及怎样从开源中获利,鼓励还在小心翼翼保护自己核心代码的公司们拥抱开源,还时不时的痛斥微软、IBM之类的闭源顽固大厂。《开垦心智层》一章读我的懵懵懂懂,什么意识形态啊,礼物文化啊,这些算是哲学范畴里的东西吗?不过《黑客反击》一章让我感觉一下子这些开源变得不那么纯粹了。为了不让自己立下的flag打脸,刻意通过各种手段(到没有非法手段)“宣传”开源文化,试图然后通过帮助网景成功来证明作者的开源理论是站得住脚的。难道不应该秉持酒香不怕巷子深的理念才更开源吗?</p>
<p>还需要重读一下这本书,后面有新的想法再补充上吧。希望能参与到开源世界中(那么我的动机是什么?)。</p>tunzaotunzao@live.cn浏览开发者头条的时候看到一篇题为《系统架构》读书笔记的文章,突然想到自己刚读完《大教堂与集市》,还没有留下一点点观后感之类的文字,没有对书本的思考,这本书就白读了吧,就像之前的《黑客与画家》,现在都记不清它都有些什么内容了。所以决定还是写点读后感类的内容。The total number of locks exceeds the lock table size2018-07-11T00:00:00+00:002018-07-11T00:00:00+00:00http://tunzao.me/articles/the-total-number-of-locks-exceeds-the-lock-table-size<p>全表更新若干个字段信息,数据量不大,也就287338条,但是反复更新都以报错失败:<code class="language-plaintext highlighter-rouge">The total number of locks exceeds the lock table size</code>。</p>
<p>Google之,问题原因是<code class="language-plaintext highlighter-rouge">innodb_buffer_pool_size</code>的默认在只有8M太小,调大就好了,试图通过<code class="language-plaintext highlighter-rouge">SET GLOBAL innodb_buffer_pool_size = 1024 * 1024 * 64;</code>的方式调整(因为这样不用重启MySQL,而且也没有重启MySQL的权限),因为不是动态变量,返回变量只读,只能通过修改my.cnf配置文件来实现了,在[mysqld]下新增<code class="language-plaintext highlighter-rouge">innodb_buffer_pool_size=64M</code>,重启MySQL。</p>
<p>然后问题就解决了,但是这个参数到底是干嘛的?lock table size 跟 <code class="language-plaintext highlighter-rouge">innodb_buffer_pool_size</code> 又有什么关系呢?</p>
<p>在参考链接里找到了答案,<code class="language-plaintext highlighter-rouge">innodb_buffer_pool_size</code>是用来指定InnodDB缓存表的数据和索引使用的内存大小,pool越大磁盘交互就会越少,性能也会越好,如果服务器是MySQL服务专用,竟然推荐设置为内存的80%,难怪线上MySQL服务的内存使用率一直稳定地维持在86%左右。InnoDB的行锁基于存储在buffer pool里的lock table来实现,有点没太读懂实现细节,虽然只是简短的一个where从句,直接贴上原话吧。</p>
<blockquote>
<p>“in Innodb row level locks are implemented by having special lock table, located in the buffer pool where small record allocated for each hash and for each row locked on that page bit can be set.”</p>
</blockquote>
<p>(参考链接里作者对其所以然的追求真是让人敬佩啊)。</p>
<p>另外在重启数据库过程中,停止数据库失败了,错误信息是<code class="language-plaintext highlighter-rouge">Timeout error occurred trying to stop MySQL Daemon.</code>,最后暴力的执行<code class="language-plaintext highlighter-rouge">killall -9 mysqld</code>把MySQL给停了。那么为什么会报这个错呢?能通过参数增大timeout的值从而让MySQL正常停止吗?</p>
<p><em>注: 数据库版本5.1.73-log</em></p>
<p>参考</p>
<p><a href="https://mrothouse.wordpress.com/2006/10/20/mysql-error-1206/">MySQL Error 1206</a></p>tunzaotunzao@live.cn全表更新若干个字段信息,数据量不大,也就287338条,但是反复更新都以报错失败:The total number of locks exceeds the lock table size。ProxyJump2017-08-09T00:00:00+00:002017-08-09T00:00:00+00:00http://tunzao.me/articles/proxy-jump<p>为了安全起见,线上机器一般不会允许用户通过ssh直接登录,而是需要通过堡垒机(跳板机)跳转到目标线上机器,这样方便对权限控制的管理和访问操作的审计。不知道公司用的是哪款堡垒机产品,选择目标机器的过程异常繁琐。查看<code class="language-plaintext highlighter-rouge">ssh</code>的手册(manual)看到了<code class="language-plaintext highlighter-rouge">-J</code>选项,可以通过该选项指定ProxyJump然后直接登录到目标机器,命令如下:</p>
<figure class="highlight"><pre><code class="language-shell" data-lang="shell">ssh <span class="nt">-J</span> user@jump.tunzao.me:80 user2@a.tunzao.me</code></pre></figure>
<p>不过要scp文件到目标机器上就有点困难了(可以通过<code class="language-plaintext highlighter-rouge">-o</code>选项指定ssh_option,但是还没弄明白怎么指定),而且每次登录都输这么长一串字符串实在是难以忍受,减少敲击次数的一个方案是<code class="language-plaintext highlighter-rouge">alias</code>,另一个方案是编辑<code class="language-plaintext highlighter-rouge">~/.ssh/config</code>文件配置ProxyJump,指定某些机器通过堡垒机登录,格式如下:</p>
<figure class="highlight"><pre><code class="language-config" data-lang="config"><span class="c">### 堡垒机
</span><span class="n">Host</span> <span class="n">jump</span>
<span class="n">HostName</span> <span class="n">jump</span>.<span class="n">tunzao</span>.<span class="n">me</span>
<span class="n">Port</span> <span class="m">80</span>
<span class="n">User</span> <span class="n">user</span>
<span class="c">### 目标机器,通过堡垒机登录
</span><span class="n">Host</span> <span class="n">a</span>
<span class="n">HostName</span> <span class="n">a</span>.<span class="n">tunzao</span>.<span class="n">me</span>
<span class="n">ProxyJump</span> <span class="n">jump</span></code></pre></figure>
<p>这样就能通过 <code class="language-plaintext highlighter-rouge">ssh user2@a</code> 登录 <code class="language-plaintext highlighter-rouge">a.tunzao.me</code> 了,同时也能通过 <code class="language-plaintext highlighter-rouge">scp afile.txt user2@a:~</code> 拷贝文件到 <code class="language-plaintext highlighter-rouge">a.tunzao.me</code> 上。Host指令后的 <code class="language-plaintext highlighter-rouge">jump</code> 同时可以用作ssh的别名,如果要使用用户 <code class="language-plaintext highlighter-rouge">user</code> 登录到 <code class="language-plaintext highlighter-rouge">jump.tunzao.me</code> 直接执行 <code class="language-plaintext highlighter-rouge">ssh jump</code> 即可。</p>
<p>参考:</p>
<p><a href="https://wiki.gentoo.org/wiki/SSH_jump_host">SSH jump host</a></p>
<p><a href="https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Proxies_and_Jump_Hosts#Passing_Through_One_or_More_Gateways_Using_ProxyJump">OpenSSH/Cookbook/Proxies and Jump Hosts</a></p>tunzaotunzao@live.cn为了安全起见,线上机器一般不会允许用户通过ssh直接登录,而是需要通过堡垒机(跳板机)跳转到目标线上机器,这样方便对权限控制的管理和访问操作的审计。不知道公司用的是哪款堡垒机产品,选择目标机器的过程异常繁琐。查看ssh的手册(manual)看到了-J选项,可以通过该选项指定ProxyJump然后直接登录到目标机器,命令如下:Tomcat NIO Connector2016-06-25T00:00:00+00:002016-06-25T00:00:00+00:00http://tunzao.me/articles/tomcat-nio-connector<p>Tomcat服务请求量巨大时connector线程数剧增。 Tomcat默认的connector是阻塞模式的(即BIO), 每次请求都需要一个单独线程处理,另外对于<code class="language-plaintext highlighter-rouge">keep-alive</code>的HTTP请求,<!--more--> BIO的connector在完成一次请求后继续等待下一次请求, 如果已完成的请求没有设置<code class="language-plaintext highlighter-rouge">Connection: close</code>并且没有关闭连接, 这个connector线程就会等待<code class="language-plaintext highlighter-rouge">keep-alive</code>设置的超时时间,然后回到线程池,性能非常低(既然Tomcat提供了更高效的connector为什么还要把它设置成默认呢?即便是Tomcat7里也是如此)。为此Tomcat还提供了NIO的connector,基于 Java 的NIO特性实现一个线程处理多个连接的功能,这样在大量请求的时候就减缓了线程的上涨速度。另外在处理<code class="language-plaintext highlighter-rouge">keep-alive</code>的请求时,每当一个connector线程处理完请求后立即被放回线程池,避免了不必要的等待时间(<code class="language-plaintext highlighter-rouge">keep-alive</code>)。</p>
<p>启用NIO的方式是修改Tomcat的<code class="language-plaintext highlighter-rouge">server.xml</code>配置文件,把connector的protocol改为NIO:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="nt"><Connector</span> <span class="na">connectionTimeout=</span><span class="s">"20000"</span> <span class="na">maxThreads=</span><span class="s">"1000"</span> <span class="na">port=</span><span class="s">"8080"</span>
<span class="na">protocol=</span><span class="s">"org.apache.coyote.http11.Http11NioProtocol"</span> <span class="na">redirectPort=</span><span class="s">"8443"</span><span class="nt">/></span></code></pre></figure>
<p>Connector默认配置成HTTP/1.1的原因:
即便是配置成BIO,connector也不一定是BIO的,如果操作系统环境变量里配置了Tomcat native lib, Tomcat就会使用ARP connector。ARP使用操作系统的本地接口,性能和伸缩性上都会比使用Java的接口要好。</p>
<p>参考:</p>
<p><a href="https://dzone.com/articles/understanding-tomcat-nio">Understanding the Tomcat NIO Connector and How to Configure It</a></p>
<p><a href="http://stackoverflow.com/questions/11032739/what-is-the-difference-between-tomcats-bio-connector-and-nio-connector">What is the difference between Tomcat’s BIO Connector and NIO Connector?</a></p>
<p><a href="https://tomcat.apache.org/tomcat-6.0-doc/config/http.html">Apache Tomcat Configuration Reference - The HTTP Connector</a></p>
<p><a href="http://publib.boulder.ibm.com/wasce/V2.1.0/en/http-connector.html">Configuring an HTTP connector</a></p>tunzaotunzao@live.cnTomcat服务请求量巨大时connector线程数剧增。 Tomcat默认的connector是阻塞模式的(即BIO), 每次请求都需要一个单独线程处理,另外对于keep-alive的HTTP请求,