AI 年度盘点与2025发展趋势展望,50+案例解析亮相AICon 了解详情
写点什么

稳定性全系列(一):如何做好系统稳定性建设

  • 2020-03-24
  • 本文字数:7838 字

    阅读完需:约 26 分钟

稳定性全系列(一):如何做好系统稳定性建设

1. 背景介绍

在移动互联网时代,用户群的积累比之前更容易,但同样,也会因为糟糕的用户体验,而快速流失用户,哪怕是号称独一无二的 12306 网站,也在不断优化系统来提升用户体验;而在后移动互联网的物联网时代,软件工程师需要和硬件工程师配合,来保证提供的服务稳定和可靠。对,我们的产品就是为了实现用户价值,并提供非凡用户体验


如果说良好用户体验是增长的基础,那么良好的操作性、稳定的使用体验就是用户体验的基础,排除掉软件可操作性(这一块需要依靠专业的设计师),剩下的就是客户端(这里的客户端包括各种小程序、WebApp、H5 页面等)和服务端,这一切都基于我们软件工程师来构建可靠、稳定的软件系统。然而,随着我们服务的用户量越来越多,服务复杂度也越来越高,我们的系统为了可维护性,也会在业务架构和系统架构上进行调整,现在流行的微服务架构也因此应运而生。然而微服务架构也并不是没有副作用,例如它会增加维护成本和系统稳定性建设的成本。


那么,什么是系统稳定性?这里我们引用百度百科的定义:系统稳定性是指系统要素在外界影响下表现出的某种稳定状态。为了方便,本文阐述的系统主要指软件系统。那么如何衡量系统稳定性的高与低呢?一个常用的指标就是服务可用时长占比,占比越高说明系统稳定性也越高,如果我们拿一整年的数据来看,常见的 4 个 9(99.99%)意味着我们系统提供的服务全年的不可用时长只有 52 分钟!它其实是一个综合指标,为什么这么说?因为我们在服务可用的定义上会有一些差别,常见的服务可用包括:服务无异常、服务响应时间低、服务有效(逻辑正确)、服务能正常触发 等。

2. 故障源的分类

系统的故障源一般可以分为两大类,一类是人为因素,另一类是自然因素。常见人为因素导致的故障如下:



人为因素我们要尽可能的 事前(故障发生前)避免,因为这些原因引发的事故很可能会导致数据丢失或错乱、资金受损等较严重后果,而且除了重启或修复后重新上线外没有过多有效的止损手段。人为因素导致的故障往往会导致软件工程师的内心受到严重打击,工作和专业能力受到质疑,造成“人财两空”的后果,“我拼了老命来产出,结果却给自己挖了个坑”是故障责任人内心的真实写照。


我们再来说说自然因素,自然因素受很多客观因素的影响,往往不受控制,无法避免。常见的自然因素导致的故障如下:



自然原因导致的故障可大可小,虽然无法避免,但由于没有第一责任人,避免了“人性拷问”,软件工程师可以和运维部、安全部的同学协作起来处理故障。

3. 稳定性建设四要素

“如果事情有变坏的可能,不管这种可能性有多小,它总会发生。”,残酷的墨菲定律预示着我们对自己系统提供的服务不要太乐观,接下来,我们说说如何建设系统稳定性,人为因素的根源一方面是专业能力不足,经验不足,另一方面很多都是无心之失,所以需要通过流程、规范来保住“底线”,减少人为因素导致的故障,而自然因素导致的故障往往具有突发性,需要联合多个团队协作来解决故障。


稳定性建设四要素:人、工具、预案和目标

第一要素:人

我们先来说“人”这一要素,它需要回答如下 5 个问题:


  • 谁应该参与稳定性建设?

  • 如何降低犯错的概率?

  • 如何提高稳定性意识?

  • 如何定责?

  • 如何激励?


稳定性建设工作需要老板支持,它的实施一般需要 开发、测试、运维、安全 还有 产品 等同学参与,而且主导方应该是开发、测试和运维。确定了参与方后,就可以做关键的一步:“参与稳定性建设的每个团队都需要在 OKR 中背负一部分稳定性指标”,这也是为什么说稳定性建设工作需要老板支持,因为和绩效考核相关。


稳定性工作,规范先行。OKR 的部分只是让各参与方在稳定性方面工作的投入变成合规化,平时如何去参与稳定性建设还得“有迹可循”,对于开发和测试来说就是要根据公司的当前技术体系去建设 开发规范、提测规范、测试规范、上线规范、复盘规范 等。我们拿和软件开发最相关的开发规范来说,开发规范是对开发人员的要求,让开发人员知道什么是必须要做的、什么是推荐的、什么是应该避免的。通常开发规范至少应该包括如下几个部分:


编码规范:对外接口命名方式、统一异常父类、业务异常码规范、对外提供服务不可用是抛异常还是返回错误码、统一第三方库的版本、哪些场景必须使用内部公共库、埋点日志怎么打、提供统一的日志、监控切面实现等,编码规范除了能规范开发的编码行为、避免犯一些低级错误和踩一些重复的坑外,另一个好处是让新入职的同学能快速了解公司的编码原则,这点对编码快速上手很重要。


这里再重点说一下为什么要统一异常父类和业务异常码,例如虽然不同模块(这里的模块指的是能独立部署的项目)可能有不同的异常父类,比如订单模块的异常父类是 OrderException、交易支付模块的异常父类是 TradeException,而 OrderException 和 TradeException 的父类是 BizException(当然 BizException 是定义在一个通用共公共库中的),而我们也需要去统一异常码,比如 200 代表正确的返回码,异常的返回码是 6 位数字(前 3 位代表模块,后 3 位代表异常类型),有了统一的异常父类和异常码后,很多切面就都可以由公共库来做了,比如统一的监控、统一的出入口日志打印,统一的异常拦截,压测标识透传、特殊的字段埋点等,千万别小看这些,这些能在未来持续提升研发效率,降低稳定性工作成本。


公共库使用规范:为了能对通用功能进行定制化改造和封装,公司内部肯定会有一些公共库,例如日志库、HTTP 库、线程池库、监控埋点库等,这些库都“久经考验”,已经被证实是有效且可靠的,这些就应该强制使用,当然为了适应业务的发展,这些公共库也应该进行迭代和升级。


项目结构规范:为了贯彻标准的项目结构,一方面我们需要为各种类型项目通过“项目脚手架”来创建标准的项目结构原型,然后基于这个项目原型来进行开发,统一的项目结构一个最显著的好处是让开发能快速接手和了解项目,这种对于团队内维护多个项目很重要,人员能进行快速补位。


数据库规范:数据库连接资源堪比 CPU 资源,现在的应用都离不开数据库,而且通常数据库都属于核心资源,一旦数据库不可用,应用都没有太有效的止损手段,所以在数据库规范里,库名、表名、索引、字段、分库分表的一些规范都必须明确。


这里特别提一点,就是分表数量不要用 2 的幂(比如 1024 张表,很多人认为使用 2 的幂分表数在计算分表时用位运算会更快,但这个开销相比数据库操作其实可以忽略),而应该用质数(比如最接近 1024 的质数应该是 1019),采用质数分表数能让数据分的更均匀。


这会引发另一个问题,那就是我们有这些规范,那么如何让开发来知晓和遵守?一方面是设定合理的奖惩机制(例如由于没有遵守规范而引发的线上事故要严惩),另一方面就是——考试!没错,就是考试,将这些规范和历史的线上事故整理成试题,让新老开发定期去考试,考试是一种传统的考核机制,我们可以把规范和公共库的更新部分,也及时加入到考试试题中,来督促大伙及时学习。


有了 OKR、规范和考核机制,加上我们定期宣导,相信各成员的稳定性意识会有显著提高。


事故定责一般是比较复杂的过程,除非事故原因非常简单明了,但实际上事故原因常常涉及多个团队,如果责任分摊不合理,难免会引发跨团队的争吵,合理的做法是引入第三方稳定性团队来干预,例如滴滴的星辰花团队,星辰花会撰写定责指南,并制定一些相关流程机制。


当然,如果达成稳定性年度目标,也应该对这些团队进行适当表彰。

第二要素:工具

工具意味着手段,要做好稳定性建设,强大的支持工具和平台是不可缺少的,常见的工具和平台包括:日志采集分析检索平台(例如滴滴的 Arius)、监控告警平台(例如滴滴的 Odin Metrics)、分布式追踪系统(例如 Google 的 Dapper、滴滴的把脉平台)、自动化打包部署平台(例如滴滴的 One Experience)、服务降级系统(例如滴滴的 SDS)、预案平台(例如滴滴的 911 预案平台)、根因定位平台(记录所有故障发生前所有系统变更事件)、放火平台等。


强大的工具能回答如下 3 个关键问题:


  • 我们能做什么?

  • 我们能做到什么程度?

  • 如何降低稳定性工作成本?


工具本质上是手段,它能降低我们在稳定性工作上投入的成本,例如有了监控告警平台,我们就不需要专人时刻盯着日志或大盘,有了分布式追踪系统,问题定位会更有效率,有了降级系统,一些故障能自动控制和恢复,不用我们再上线一次。要想做好稳定性工作,工具必不可少,没有工具,稳定性建设总是低效的。


其实公司内建的公共库也属于工具的一种,像滴滴内部的公共库,业务系统接入 Odin Metrics 和把脉几乎不要做额外的工作(当然接入把脉需要提日志采集工单,头疼),千万不要吝啬在工具方面的投入,很多开源框架可以拿来用或拿来参考,工具和平台可以内部进行互通和联动,这样可以建成一站式的稳定性工作平台。

第三要素:预案

紧急预案是我们在故障发生时的行动指南,这在故障可能涉及到多个团队、故障进展需要周知到多个团队时特别有用。


完善的紧急预案能回答如下 4 个问题:


  • 故障发生时我们该做什么?

  • 谁来指挥?

  • 谁来决策?

  • 我们如何善后?


当一个不那么容易定位的故障发生时,你应该做的第一件事应该是什么?这在不同公司、同一个公司同一个团队的不同成员恐怕都会给出一个不同的答案,而在滴滴内部,我们大多会第一时间通知团队内其他成员、Leader(寻求帮助)和客服、上游业务开发等可能的影响方 (问题周知)。


当这一步做完以后,一般就会有一部分同学加入问题排查和止损,然而介入的人多了,排查和止损的效率不一定会成比例的提升,这时候协调者很重要,协调者要避免介入的同学在做重复工作,协调者还需要持续和客服、上游业务开发等影响方沟通(我们曾经就经历过由于问题排查问题进度没有及时有效和业务方沟通,业务方将故障升级的 case)。对于排查问题和止损的同学来说,要操作某个开关,有可能还要去查代码看开关的名字是什么,还有可能关掉一个功能需要操作多个开关,这些在紧急时刻都有可能由于慌乱而出错。而且什么条件下才能操作开关,谁能决定应不应该操作开关,恐怕在当时很难去做最正确的事情,而这一切,没错,都应该提前写到预案中!!!


紧急预案一般要包含如下内容:


  • 故障发生时应该通知哪些人或团队。

  • 如何选出协调者,什么情况下该选出协调者。

  • 协调者的职责有哪些。

  • 需要操作开关时,谁有权利决策。

  • 常见故障以及对应的止损方式。

  • 止损的原则是什么,什么是最重要的。善后方案谁来拍板。


预案很重要,完备的预案能降低故障定位和止损的时间,提升协作效率。

第四要素:目标

如何衡量稳定性建设工作是有价值的?如何考核稳定性建设工作达没达标、做的好不好?这些都能在稳定性建设的目标中找到答案。


稳定性建设的目标主要用来回答如下 2 个问题:


  • 稳定性工作的价值是什么?

  • 稳定性工作如何考核?


稳定性建设工作的价值不仅需要团队所有成员认可,更重要的是需要老板的认可,没有老板的认可,稳定性建设工作只是团队内部的“小打小闹”,难以去跨团队来体系化运作。


稳定性建设工作的年度目标可以拿服务可用时长占比来定,也可以拿全年故障等级和次数来定,像滴滴这边,星辰花将故障等级分成了 P0 至 P5 六个等级,P0、P1、P2 属于重大事故,是需要消耗服务不可用时长的(根据全年定的服务可用时长占比指标来计算出某个部门的全年服务不可用总时长),一旦年底某个部门的全年服务不可用时长超过年初设定的阈值,就会有一定的处罚,并影响部门绩效(之前达标也有奖励,但后来奖励取消了)。


这里做一个汇总:


4. 稳定性建设四个方向

前面我们提到的稳定性建设工作的四个关键点,但对如何落地阐述的并不多,这里结合作者多年的稳定性建设工作经验,谈谈稳定性建设工作的四个方向。

第一个方向:根基要抓牢(45%)

稳定性建设工作重在预防,根据作者多年的工作经验,至少 6 成线上故障都可以在预防工作中消除,我们需要投入 45%的精力来做根基建设,所谓根基建设,就是把开发、测试、上线这三大流程做透!!下面列了几个关键点:


Code Review:CR 其实是一个很重要的环节,当一个开发整个编码和提测都可以自己闭环搞定时,时间一长就容易产生懈怠,这时候写隐患代码的几率会大大提高,CR 的过程并不是 diss 的过程,这个一定要在团队内拉齐,相反,CR 是一次很好的团队沟通和塑造自己影响力的机会。我就很佩服那些代码写得质量高并且能把整个流程讲顺的人。我们团队的项目都接入了全流程(例如滴滴的鲲鹏),分支合 master 必须要其他人 Review,但这是“离线”的,没有代码作者讲的过程,效果没有几个人坐在小黑屋讲的好,只是更快而已。我们团队规定,大于等于 4 人日的项目需要进行小黑屋 CR。CR 还可以让其他成员来检测该代码实现是否遵循了开发规范,毕竟“先污染后治理”的成本太高,记住,CR 一定是一个相互学习的过程


设计评审:再也没有比糟糕的设计更有破坏力的东西了,设计评审和 CR 可以放在一起做,先评审设计再进行 CR,有人就会说,都编码完了才进行设计评审是不是晚了?其实这要看情况而定,如果团队内部经常产出“糟糕设计”,那么我觉得设计评审就应该编码之前来做,但如果团队成员专业能力和经验都还不错,那么我们允许你再编码之后再做设计评审,而且编码的过程其实也是设计的过程,可以规避提前设计而导致后续编码和设计不一致的问题。设计评审的原则是,既要讲最终的设计方案,也要讲你淘汰的设计方案


提测标准:写完代码就可以提测了?当然不是,至少得完成补充单元测试、完成自测、完成开发侧的联调、通过测试用例(如果 QA 提前给了测试用例的话)、补充改动点和影响点(便于 QA 评估测试范围)、补充设计文档(对,现在滴滴的 QA 养成了读代码、看设计的习惯)这些步骤才能说可以提测了。当然,提测标准理论上是 QA 同学来定义的。


测试流程:测试的全流程覆盖最好能做到全自动化,很多测试用例可以沉淀下来,用来做全流程回归,当然这需要系统支持。我也见过太多犹豫 QA 没精力进行全流程回归而导致问题没有提前发现而产生的事故,所以 测试的原则是尽可能自动化和全流程覆盖,让宝贵的人力资源投入到只能人工测试的环节


上线流程:上线也是一个风险很高的操作,我们简单统计了 19 年的上线次数,光我们团队负责的系统就上线了五百多次。部署平台需要支持灰度发布、小流量发布,强制让开发在发布时观察线上大盘和日志,一旦有问题,能做到快速回滚(当然要关注回滚条件)。我们这边的做法是先小流量集群灰度(我们把单量少的城市单独做了一套小流量集群),再线上灰度,确保哪怕出问题也能控制影响。

第二个方向:工作在日常(30%)

俗话说养兵一日,用兵一时,平日的养兵其实也非常重要,这一方向我们需要投入 30% 的精力,需要我们做到如下几点:


人人参与:团队内人人都需要参与稳定性建设工作,稳定性工作不是某个人的事情,所以我会要求所有人的 OKR 中都有稳定性建设的部分。做 toC 研发的同学,都养成了带电脑回家的习惯,哪怕是加班到晚上 12 点,当然在外旅游也带着电脑,手机 24 小时保持畅通;稳定性已经成为了生活本身。


持续完善监控告警:监控告警就是我们发现故障的“眼睛”和“耳朵”,然而大多数监控告警都需要我们手动一个个配置,随着业务的不断迭代,会有很多新接口需要添加监控,一些老的监控的阈值也需要不断调整(否则大量告警会让人麻木),所以监控告警是一个持续优化的过程。


及时消灭线上小隐患:平日发现的一些问题要及时消灭,很多线上事故在事前都有一定预兆,放任平时的一些小问题不管,到后面只会给未来埋上隐患。


跨团队联动:稳定性肯定不是一个团队的事情,一些降级方案可能涉及多个团队的工作,所以定期的跨团队的沟通会议是很有必要的,要大伙一起使劲才能把事情做好。


复盘机制:对出过的线上事故一定要及时的进行复盘,通过复盘来发现我们现有流程、机制是否有问题,让大伙不要踩重复的坑,并不断完善我们的紧急预案。复盘虽然属于事后的行为,但很重要,我们需要通过复盘来看下次是否能预防此故障,或者是否能更快的定位和止损。


会议机制:稳定性周会、稳定性月会,我们通过各种定期的会议来总结一些阶段性进展和成果,拉齐大家认知,这也是大伙日常稳定性工作露出的一个机会,所以非常重要。

第三个方向:预案是关键(15%)

我们通常都会忽视预案的作用,因为预案整理起来确实比较麻烦,预案也需要随着功能的迭代而不断更新,否则将很容易过时,而且预案在平日非故障期间也确实没有发挥作用的机会。但我们不得不承认紧急预案相当重要,特别是当我们去定位和止损一个比较复杂的线上问题时。我们需要在预案的制定和演练上投入 15%的精力,可以从如下三个方面着手:


分场景制定和完善紧急预案:如果我们还没有紧急预案,那第一步就是分类分场景整理下历史上经常发生的线上事故,例如 MySQL 故障预案、MQ 故障预案、发单接口故障预案等。而且预案有可能会被多人查看,一定要清晰易懂,如果某些预案是有损的,需要把副作用也描述清楚。


通过放火平台来验证预案:借助放火平台和服务降级系统,我们可以通过主动给主流程服务的非核心依赖注入故障,来验证系统在遇到非核心依赖发生故障时,核心服务是否仍旧有效,如果某些预案无法做成系统自动的(比如某些预案有一定的风险或副作用),也可以在预发环境来验证该预案是否能达到预期效果,防止真正故障发生时“手生”。预案就是在这种不断演练过程中来优化和完善的,这样的预案才是动态的,才是活生生有效可靠的

第四个方向:容量是核心(10%)

我们知道木桶效应,一个木桶能装多少水取决于最短的那块板,在分布式系统中也是如此,我们需要摸到分布式系统中的这块“短木板”才能知道整个系统的吞吐量(容量),如果我们没有这个值,老板问你明年单量要 Double,问你要预算,要规划你凭什么给?最准确的容量预估方案就是——线上全链路压测。至于滴滴是如何做线上全链路压测,后续我会有专门的文章来阐述。


我们继续探讨容量这个话题,我们应该投入 10%的精力来摸容量、扩容量、水位预警等。容量也相当重要,根据我的经验,线上有大约 10%的故障和容量有关,当遇到这种问题,最有效的解决方案就是扩容!关于容量,我们在日常需要做到如下三点:


常态化的全链路压测:线上全链路压测必须定期举行,特别的在有大促活动时,也需要提前进行一次。因为随着业务的快速迭代,系统老的瓶颈可能消失,新的瓶颈可能出现,所以之前的全链路压测的结果将失效,我们需要定期去摸这个线上环境的这个阈值。


定期进行扩容演练:在滴滴内部,我们会定期进行弹性云扩容演练,这在紧急情况下很有用,我们就曾经遇到过弹性云扩容比修改阈值重新上线更快解决问题的 case。


多活建设:我们知道多活主要是为了容灾,但其实多活实际上也从整体上增加了系统容量,所以也属于容量扩充的范畴,一旦某个机房遇到瓶颈,我们可以分流到其他机房。当然多活建设需要一定成本,业务量大到一定程度才需要投入。


说了这么多,我们也放张图来进行总结:


3. 总结

稳定性是一个大投入,做稳定性建设一定要结合公司的实际情况,量入为出,最合适的方案才是最好的方案。结合咱们上述讨论的几点,我们可以画出稳定性建设的房子,如下:



希望我们能像建筑师一样,给业务构建一套稳定、可扩展、性价比高的房子!!!


作者介绍


易振强,滴滴专家工程师


我是易振强,热爱开源,热爱分享,深耕分布式系统和稳定性建设,欢迎关注 SDS 服务降级系统:https://github.com/didi/sds ;也热爱生活,热爱漫画,一拳超人和海贼王都很好看!!


本文转载自公众号普惠出行产品技术(ID:pzcxtech)。


原文链接


https://mp.weixin.qq.com/s/R2qBQgJCueErBL35ld4KZQ


2020-03-24 10:0031478

评论 6 条评论

发布
用户头像
?????????

做 toC 研发的同学,都养成了带电脑回家的习惯,哪怕是加班到晚上 12 点,当然在外旅游也带着电脑,手机 24 小时保持畅通;稳定性已经成为了生活本身

2024-09-13 09:14 · 北京
回复
用户头像
good
2023-03-10 11:53 · 北京
回复
用户头像
产品怎么参与?砍需求?
2022-07-18 17:39
回复
用户头像
还有产品?产品感觉不太愿意参与这种事情

开发、测试、运维、安全 还有 产品

2022-01-07 16:22
回复
用户头像
抓手?
2021-01-07 10:53
回复
用户头像
稳定性

2020-10-12 19:44
回复
没有更多了
发现更多内容

DataPipeline CPO 陈雷:实时数据融合之道,博观约取,价值驱动

DataPipeline数见科技

数据融合

接口测试学习之json

测试人生路

json 接口测试

媲美物理机,裸金属云主机如何轻松应对11.11大促

京东科技开发者

云计算 容器 服务器 云主机

一致性hash算法

天涯若海

数字货币交易所开发有哪些模式?区块链交易平台

13530558032

Scrum指南这么改,我看要完蛋!

华为云开发者联盟

Scrum 敏捷 改版

区块链数字钱包系统开发方案,区块链钱包APP源码

13530558032

Springboot过滤器和拦截器详解及使用场景

996小迁

Java 编程 架构 面试 springboot

强化学习入门必看之强化学习导识

Alocasia

人工智能 学习

区块链社交即时通许系统开发,区块链社交app开发价格

13530558032

面试官问:如何排除GC引起的CPU飙高?我脱口而出5个步骤

田维常

cpu飙满

DataPipeline CTO 陈肃:构建批流一体数据融合平台的一致性语义保证

DataPipeline数见科技

数据融合

深入浅出 Go - sync.Map 源码分析

helbing

Go 语言

公众号高频被调整,它不是企业生产文章的机器

Linkflow

客户数据平台 CDP 私域流量

《JAVA多线程设计模式》.pdf

田维常

多线程

DataPipeline CPO 陈雷:实时数据融合之法,便捷可管理

DataPipeline数见科技

数据融合

快进收藏吃灰!字节跳动大佬用最通俗方法讲明白了红黑树算法

小Q

Java 学习 架构 面试 算法

OpenFeign和Consul爱恨交织的两天

编号94530

Spring Cloud Consul OpenFegin spring 5

MySQL主从数据库没有同步怎么办?

冰河

MySQL 数据库 分布式 微服务

深入浅出 Go - sync.Once 源码分析

helbing

Go 语言

架构师训练营第九周作业

我是谁

极客大学架构师训练营

深入理解h2和r2dbc-h2

程序那些事

响应式编程 R2DBC 程序那些事 响应式架构 r2dbc-h2

合约跟单源码案例,合约跟单模式开发

13530558032

UNISKIN COO Kevin|营销数字化:数据沉淀和数据系统化运营一定要趁早!

Linkflow

营销数字化 客户数据平台 CDP

微信官方将打击恶意营销号:自媒体不可过度消费粉丝

石头IT视角

阿里达摩院副院长亲自所写Java架构29大核心知识体系+大厂面试真题+微服务

Java架构追梦

Java 学习 阿里巴巴 架构 面试

Istio 1.8 发布——用户至上的选择

Jimmy Song

开源 云原生 Service Mesh istio

万字图文 | 聊一聊 ReentrantLock 和 AQS 那点事(看完不会你找我)

马丁玩编程

架构 AQS ReentrantLock JUC CLH

DataPipeline CPO 陈雷:实时数据融合之法,稳定高容错

DataPipeline数见科技

数据融合

11月阿里Spring全家桶+MQ微服务架构笔记:源码+实战

小Q

Java 学习 程序员 面试 微服务

架构师训练营第九周作业

_

极客大学架构师训练营 第九周作业

稳定性全系列(一):如何做好系统稳定性建设_移动_易振强_InfoQ精选文章