产品战略专家梁宁确认出席AICon北京站,分享AI时代下的商业逻辑与产品需求 了解详情
写点什么

AI 独角兽商汤科技的内部服务容器化历程

  • 2020-04-12
  • 本文字数:8018 字

    阅读完需:约 26 分钟

AI独角兽商汤科技的内部服务容器化历程

本文由阿尔曼,商汤科技运维工程师于 4 月 26 日晚在 Rancher 微信群所做的技术分享整理而成。商汤科技是专注于计算机视觉领域的 AI 公司。本次分享结合了容器平台团队帮助公司业务/内部服务容器化历程,介绍商汤科技在容器化历程中使用的工具、拥有的最佳实践及值得分享的经验教训。

内容目录

  • 背景

  • 需求分析与技术选型

  • 容器镜像

  • 监控报警

  • 可靠性保障

  • 总结

背景

商汤科技是一家计算机视觉领域的 AI 创业公司,公司内会有一些业务需要云端 API 支持,一些客户也会通过公网调用这些所谓 SaaS 服务。总体来讲,云 API 的架构比较简单,另外由于公司成立不久,历史包袱要轻许多,很多业务在设计之初就有类似微服务的架构,比较适合通过容器化来适配其部署较繁复的问题。


公司各个业务线相对独立,在组织上,体现在人员,绩效及汇报关系的差异;在技术上体现在编程语言,框架及技术架构的独自演进,而服务的部署上线和后续维护的工作,则划归于运维部门。这种独立性、差异性所加大的运维复杂度需要得到收敛。


我们遇到的问题不是新问题,业界也是有不少应对的工具和方法论,但在早期,我们对运维工具的复杂性增长还是保持了一定的克制:ssh + bash script 扛过了早期的一段时光,ansible 也得到过数月的应用,但现实所迫,我们最终还是投向了 Docker 的怀抱。


Docker 是革命性的,干净利落的 UX 俘获了技术人员的芳心,我们当时所处的时期,容器编排的大战则正处于 Docker Swarm mode 发布的阶段,而我们需要寻找那种工具,要既能应对日益增长的运维复杂度,也能把运维工程师从单调、重复、压力大的发布中解放出来。


Rancher 是我们在 HackerNews 上的评论上看到的,其简单易用性让我们看到了生产环境部署容器化应用的曙光,但是要真正能放心地在生产环境使用容器,不“翻车”,还是有不少工作要做。由于篇幅的原因,事无巨细的描述是不现实的。我接下来首先介绍我们当时的需求分析和技术选型,再谈谈几个重要的组成部分如 容器镜像、监控报警和可靠性保障

需求分析与技术选型

暂时抛开容器/容器编排/微服务这些时髦的词在一边,对于我们当时的情况,这套新的运维工具需要三个特性才能算成功:开发友好、操作可控及易运维

开发友好

能把应用打包的工作推给开发来做,来消灭自己打包/编译如 java/ruby/python 代码的工作,但又要保证开发打出的包在生产环境至少要能运行,所以怎么能让开发人员方便正确地打出发布包,后者又能自动流转到生产环境是关键。长话短说,我们采取的是 Docker + Harbor 的方式,由开发人员构建容器镜像,通过 LDAP 认证推送到公司内部基于 Harbor 的容器镜像站,再通过 Harbor 的 replication 机制,自动将内部镜像同步到生产环境的镜像站,具体实现可参考接下来的 容器镜像 一节。

操作可控

能让开发人员参与到服务发布的工作中来,由于业务线迥异的业务场景/技术栈/架构,使得只靠运维人员来解决发布时出现的代码相关问题是勉为其难的,所以需要能够让开发人员在受控的情境下,参与到服务日常的发布工作中来,而这就需要像其提供一些受限可审计且易用的接口,WebUI+Webhook 就是比较灵活的方案。这方面,Rancher 提供的功能符合需求。

易运维

运维复杂度实话说是我们关注的核心,毕竟容器化是运维部门为适应复杂度与日俱增而发起的,屁股决定脑袋。考虑到本身容器的黑盒性和稳定性欠佳的问题,再加上真正把容器技术搞明白的人寥寥无几,能平稳落地的容器化运维在我们这里体现为三个需求:多租户支持,稳定且出了事能知道,故障切换成本低。多租户是支持多个并行业务线的必要项;容器出问题的情况太多,线上环境以操作系统镜像的方式限定每台机器 Docker 和内核版本;由于传统监控报警工具在容器化环境捉襟见肘,需要一整套新的监控报警解决方案;没人有把握能现场调试所有容器问题(如跨主机容器网络不通/挂载点泄漏/dockerd 卡死/基础组件容器起不来),需要蓝绿部署的出故障后能立刻切换,维护可靠与可控感对于一个新系统至关重要。

技术架构图

总结一下,Rancher, Harbor, Prometheus/Alertmanager 为主的开源系统组合可以基本满足容器管理的大部分需求,总体架构如下图


容器镜像

容器镜像服务是公司级别的 IT 基础设施,在各个办公区互联带宽有限的物理限制下,需要给分散在多个地理位置的用户以一致、方便、快速的使用体验。我们主要使用了 Vmware 开源的 Harbor 工具来搭建容器镜像服务,虽然 Harbor 解决了如认证、同步等问题,但 Harbor 不是这个问题的银色子弹,还是需要做一些工作来使镜像服务有比较好的用户体验。这种体验我们以 Google Container Registry 为例来展现。


作为 Google 的开放容器镜像服务,全球各地的用户都会以同一个域名 gcr.io 推拉镜像docker push gcr.io/my_repo/my_image:my_tag,但其实用户推拉镜像的请求,由于来源地理位置不同,可能会被 GeoDNS 分发在不同的 Google 数据中心上,这些数据中心之间有高速网络连接,各种应用包括 GCR 会通过网络同步数据。这样的方法既给用户一致的使用体验,即所有人都是通过 gcr.io 的域名推拉镜像,又因为每个人都是同自己地理位置近的数据中心交互而不会太“卡”,并且由于 Google Container Registry 底层存储的跨数据中心在不断高速同步镜像(得益于 Google 优异的 IT 基础设施),异国他乡的别人也能感觉很快地拉取我们推送的镜像(镜像“推”和“拉”的异步性是前提条件)。


花篇幅介绍 Google Container Registry 的目的是,用户体验对用户接受度至关重要,而后者往往是一个新服务存活的关键,即在公司内部提供类似 GCR 一般的体验,是我们容器镜像服务为了成功落地而想接近的产品观感。为了达到这种观感,需要介绍两个核心的功能,开发/生产镜像自动同步,镜像跨办公区同步。另外,虽然有点超出镜像服务本身,但由于特殊的国情和使用关联性,国外镜像(DockerHub, GCR, Quay)拉取慢也是影响容器镜像服务使用体验的关键一环,镜像加速服务 也是需要的。

开发/生产镜像自动同步

由于开发环境(公司私网),生产环境(公网)的安全性和使用场景的差异,我们部署了两套镜像服务,内网的为了方便开发人员使用是基于 LDAP 认证,而公网的则做了多种安全措施来限制访问。但这带来的问题是如何方便地向生产环境传递镜像,即开发人员在内网打出的镜像需要能自动地同步到生产环境。


我们利用了 Harbor 的 replication 功能,只对生产环境需要的项目才手动启用了 replication,通过这种方式只需初次上线时候的配置,后续开发的镜像推送就会有内网 Harbor 自动同步到公网的 Harbor 上,不需要人工操作。

镜像跨办公区同步

由于公司在多地有办公区,同一个 team 的成员也会有地理位置的分布。为了使他们能方便地协作开发,镜像需要跨地同步,这我们就依靠了公司已有的 swift 存储,这一块儿没有太多可说的,带宽越大,同步的速度就越快。值得一提的是,由于 Harbor 的 UI 需要从 MySQL 提取数据,所以如果需要各地看到一样的界面,是需要同步 Harbor MySQL 数据的。

镜像加速

很多开源镜像都托管在 DockerHub、Google Container Registry 和 Quay 上,由于受制于 GFW 及公司网络带宽,直接 pull 这些镜像,速度如龟爬,极大影响工作心情和效率。


一种可行方案是将这些镜像通过代理下载下来,docker tag后上传到公司镜像站,再更改相应 manifest yaml,但这种方案的用户体验就是像最终幻想里的踩雷式遇敌,普通用户不知道为什么应用起不了,即使知道了是因为镜像拉取慢,镜像有时能拉有时又不能拉,他的机器能拉,我的机器不能拉,得搞明白哪里去配默认镜像地址,而且还得想办法把镜像从国外拉回来,上传到公司,整个过程繁琐耗时低智,把时间浪费在这种事情上,实在是浪费生命。


我们采取的方案是,用 mirror.example.com 的域名来 mirror DockerHub,同时公司 nameserver 劫持 quay,gcr,这样用户只需要配置一次 docker daemon 就可以无痛拉取所有常用镜像,也不用担心是否哪里需要 override 拉取镜像的位置,而且每个办公区都做类似的部署,这样用户都是在办公区本地拉取镜像,速度快并且节约宝贵的办公区间带宽。


值得一提的是,由于对 gcr.io 等域名在办公区内网做了劫持,但我们手里肯定没有这些域名的 key,所以必须用 http 来拉取镜像,于是需要配置 docker daemon 的--insecure-registry这个项


用户体验


配置 docker daemon(以 Ubuntu 16.04 为例)


sudo -scat << EOF > /etc/docker/daemon.json{  "insecure-registries": ["quay.io", "gcr.io","k8s.gcr.io],  "registry-mirrors": ["https://mirror.example.com"]}EOFsystemctl restart docker.service
复制代码


测试


# 测试解析,应解析到一个内网IP地址(private IP address)# 拉取dockerhub镜像docker pull ubuntu:xenial# 拉取google镜像docker pull gcr.io/google_containers/kube-apiserver:v1.10.0# 拉取quay镜像docker pull quay.io/coreos/etcd:v3.2# minikubeminikube start --insecure-registry gcr.io,quay.io,k8s.gcr.io --registry-mirror https://mirror.example.com
复制代码

技术架构图

监控报警

由于 zabbix 等传统监控报警工具容器化环境中捉襟见肘,我们需要重新建立一套监控报警系统,幸亏 prometheus/alertmanager 使用还算比较方便,并且已有的 zabbix 由于使用不善,导致已有监控系统的用户体验很差(误报/漏报/报警风暴/命名不规范/操作复杂等等),不然在有限的时间和人员条件下,只是为了 kick start 而什么都得另起炉灶,还是很麻烦的。


其实分布式系统的监控报警系统,不论在是否用容器,都需要解决这些问题:能感知机器/容器(进程)/应用/三个层面的指标,分散在各个机器的日志要能尽快收集起来供查询检索及报警低信噪比、不误报不漏报、能“望文生义”等。


而这些问题就像之前提到的,prometheus/alertmanager 已经解决得比较好了:通过 exporter pattern,插件化的解决灵活适配不同监控目标(node-exporter, cAdvisor, mysql-exporter, elasticsearch-exporter 等等);利用 prometheus 和 rancher dns 服务配合,可以动态发现新加入的 exporter/agent;alertmanager 则是一款很优秀的报警工具,能实现 alerts 的路由/聚合/正则匹配,配合已有的邮件和我们自己添加的微信(现已官方支持)/电话(集成阿里云语音服务),每天报警数量和频次达到了 oncall 人员能接受的状态。


至于日志收集,我们还是遵从了社区的推荐,使用了 Elasticsearch + fluentd + Kibana 的组合,fluentd 作为 Rancher 的 Global Serivce(对应于 Kubernetes 的 daemon set),收集每台机器的系统日志,dockerd 日志,通过docker_metadata这个插件来收集容器标准输出(log_driver: json_file)的日志,rancher 基础服务日志,既本地文件系统压缩存档也及时地发往相应的 elasticsearch 服务(并未用容器方式启动),通过 Kibana 可视化供产品售后使用。基于的日志报警使用的是 Yelp 开源的 elastalert 工具。


为每个环境手动创建监控报警 stack 还是蛮繁琐的,于是我们也自定义了一个 Rancher Catalog 来方便部署。


监控报警系统涉及的方面太多,而至于什么是一个“好”的监控报警系统,不是我在这里能阐述的话题,Google 的 Site Reliability Engineering 的这本书有我认为比较好的诠释,但一个抛砖引玉的观点可以分享,即把监控报警系统也当成一个严肃的产品来设计和改进,需要有一个人(最好是核心 oncall 人员)承担产品经理般的角色,来从人性地角度来衡量这个产品是否真的好用,是否有观感上的问题,特别是要避免破窗效应,这样对于建立 oncall 人员对监控报警系统的信赖和认可至关重要。

技术架构图

可靠性保障

分布式系统在提升了并发性能的同时,也增大了局部故障的概率。健壮的程序设计和部署方案能够提高系统的容错性,提高系统的可用性。可靠性保障是运维部门发起的一系列目的在于保障业务稳定/可靠/鲁棒的措施和方法,具体包括:


  • 生产就绪性检查

  • 备份管理体系

  • 故障分析与总结

  • chaos monkey


主要谈谈 chaos monkey,总体思路就是流水不腐,户枢不蠹。通过模拟各种可能存在的故障,发现系统存在的可用性问题,提醒开发/运维人员进行各种层面的改进。

预期

  • 大多数故障无需人立刻干预

  • 业务异常(如 HTTP 502/503)窗口在两分钟以内

  • 报警系统应该保证

  • 不漏报

  • 没有报警风暴

  • 报警分级别(邮件/微信/电话)发到该接收报警的人

测试样例

我们需要进行测试的 case 有:


  • service 升级

  • 业务容器随机销毁

  • 主机遣散

  • 网络抖动模拟

  • Rancher 基础服务升级

  • 主机级别网络故障

  • 单主机机器宕机

  • 若干个主机机器宕机

  • 可用区宕机

部署示例(单个租户 & 单个地域)

总结

1、体量较小公司也可以搭建相对可用的容器平台。


2、公司发展早期投入一些精力在基础设施的建设上,从长远来看还是有价值的,这种价值体现在很早就可以积累一批有能力有经验有干劲儿的团队,来不断对抗规模扩大后的复杂性猛增的问题。一个“让人直观感觉”,“看起来”混乱的基础技术架构,会很大程度上影响开发人员编码效率。甚至可以根据破窗原理揣测,开发人员可能会觉得将会运行在“脏”,“乱”,”差”平台的项目没必要把质量看得太重。对于一个大的组织来讲,秩序是一种可贵的资产,是有无法估量的价值的。


3、镜像拉取慢问题也可以比较优雅地缓解。


4、国内访问国外网络资源总体来讲还是不方便的,即使没有 GFW,带宽也是很大的问题。而我们的解决方案也很朴素,就是缓存加本地访问,这样用比较优雅高效地方法解决一个“苍蝇”问题,改善了很多人的工作体验,作为工程人员,心里是很满足的。


5、容器化也可以看作是一种对传统运维体系的重构。


6、容器化本质上是当容器成为技术架构的所谓 building blocks 之后,对已有开发运维解决方案重新审视,设计与重构。微服务、云原生催生了容器技术产生,而后者,特别是 Docker 工具本身美妙的 UX,极大地鼓舞了技术人员与企业奔向运维“应许之地”的热情。虽然大家都心知肚明银色子弹并不存在,但 Kubernetes ecosystem 越来越看起来前途不可限量,给人以无限希望。而贩卖希望本身被历史不断证明,倒真是稳赚不亏的商业模式。

致谢

1、感谢 Richard Stallman 为代表的自由软件运动的参与者、贡献者们,让小人物、小公司也能有大作为。


2、感谢 Google Search 让搜索信息变得如此便利。


3、感谢 Docker 公司及 Docker 软件的贡献者们,催生了一个巨大的行业也改善了众多开发/运维人员的生活。


4、感谢 Rancher 这个优秀的开源项目,提供了如 Docker 般的容器运维 UX。


5、感谢 GitHub 让软件协作和代码共享如此便利和普及。


6、感谢 mermaid 插件的作者们,可以方便地用 markdown 定义编辑好看的流程图。

Q&A

Q:你们线上是直接用的测试环境同步的镜像吗?


A:是的,这也是社区推荐的实践,同一个镜像流转开发/测试/预发布/生产环境,可以预先提供一些安全/小型的 base image 给开发人员,做好配置文件与代码的隔离就好。


Q:如何解决不同环境的配置问题?


A:为了保证同一个镜像流转开发/测试/预发布/生产环境,所以需要首先把配置与代码分离,配置可以通过环境变量,side-kick container 或者 rancher-secret 的方式传入代码所在镜像。


Q:作为一家 AI 公司,是否有使用容器来给开发者构建一个机器学习的平台?


A:商汤在比较早期就开始跑分布式的深度训练集群,当时容器/编排还不是一个 feasiable 的 solution,对于异构的架构支持也不太好,比如 nvidia gpu 的 device plugin 也是最近才发布,所以容器还不是我们生产训练集群的基础技术,但是我们迭代的方向。


Q:你好,关于拉取墙外的镜像这个地方我有点不太清楚,最终肯定还是需要一个可以翻出去的节点去拉取镜像吧?


A:对,有多种方法做到这一点。最简单的方法就是利用 http 代理的方式,网络层的 vpn 也可以。


Q:数据库你们也放 docker 里么?现在我看到有些人也把 mysql 放 docker 里,这种方案你们研究过么?可行性如何?


A:有状态的应用跑在容器里本身就是一个复杂的问题,kubernetes 也是引入了 operator pattern 才在几个有状态的应用(etcd/prometheus)上有比较好的效果,operator 的代码量也是相对庞大的,rancher/cattle 作为轻量级的解决方案,还是适合 web 类型的应用跑在容器里。


Q:业务整体融合到 Rancher 中遇到过什么问题吗?


A:这个问题就太宽泛了,遇到的问题有很多,技术非技术都会有,我可以讲一个例子,比如 java 应用跑在容器里,我们就会遇到类似https://developers.redhat.com/blog/2017/03/14/java-inside-docker/这样的问题,可以问的更具体一些。


Q:普罗米修斯的 catalog 能否共享一下?


A:这个 catalog 我们就是按 Rancher 官方 catalog 里的 Prometheus 改的,增加了 fluentd/额外的 exporter,定制了镜像之类的,没有什么 magic。


Q:普罗米修斯和 altermanger 有没有相关文档?


A:prometheus/alertmanager 说实话,是 poorly documented,alertmanager 我们是代码好好看过的,prometheus 的查询语句也是不太好写,这一点没啥好办法,多看多尝试吧。


Q:harbor 里面的镜像,贵公司是怎么批量删除的?


A:目前还没有这个刚需,但确实是需要考虑的,我这里还没啥想法。


Q:接口监控是怎么做的?网络抖动用什么模拟的?


A:接口监控我们做的比较粗糙,用的 blackbox-exporter,需要手动添加,目前监控报警系统我们在深度定制中,目标是做成向 opsgenie 这样的体验;网络抖动是用https://github.com/alexei-led/pumba 这个工具做的。


Q:你们的服务可用性达到了一个什么样的级别呢?有没有出现过什么比较大的事故?


A:目前各个服务上线都不久,谈可用性就比较虚了;比较大的事故的话,我们曾经遇到 rancher 的一个 bug(https://github.com/rancher/rancher/issues/9118),还有应用没有好好配健康检查,服务进程 PID 不为 1,大量 503 这样的,我们每次大的事故都会做 postmortem,早期还不少的,主要是经验和测试不够的问题。


Q:请问 prometheus 用的是什么存储,有没有考虑数据高可用这块?


A:prometheus 我们就是用的普通的 local storage,升级就会丢失,考虑过数据高可用,后续考虑 remote storage。


Q:您在分享中提到了一个 alertmanage,这个产品必须配合 prometheus 使用吗?


A:这不一定的,我们还用 alertmanager 直接接受 zabbix 发出的报警,alertmanager 提供 HTTP 的接口的https://prometheus.io/docs/alerting/clients/


Q: 请问多租户是如何实现的?


A: 我们是利用 rancher 的 enviroments 做多租户的,每个环境一个租户(其实为了可灵活切换/基础组件升级,每个租户会有两个几乎一样的环境)。


Q:生产环境上 k8s 的话,采用哪种部署方式比较好?


A:我觉得 rancher 2.0 就是一个很好的方案,很适合企业需求,部署的话 rke 真的蛮好使的(之前我都不信),比 kubespray 好使多了。


Q:普罗米修斯里面的 nodeexporter 和 cadvicor 都是 overlay 网络的地址吧。如何和宿主机对应上呢?每次找起来挺费劲的。


A:这个是好问题,这两个直接用 host network,然后勾选 cattle 的 Enable Rancher DNS service discovery 这个选项,来让 rancher dns 服务应用到不使用 managed network 的服务就好。


Q:Prometheus remote storage 你们选择的是什么数据库呢?


A:remote storage 我们还没有正式使用。


Q:Elastic kibana 的安全你们是怎么做的?ELK 的企业版么?


A:一般来讲这个可以先在 nginx 里 disable delete 方法,再配合 basic auth 来做,有的 team 使用了 searchguard 这个插件。


Q:请问你们的服务暴露用 service 做 nodeport 还是 ingress?


A:我们生产还没有使用 kubernetes,rancher 的话可以考虑使用 Kong 或者 rancher loadbalancer 直接绑主机端口。


Q:efk 的日志数据用的什么存储?贵司维护 rancher 的团队有多少人?


A:fluentd 会在本地文件系统压一份,再往 elasticsearch 打一份(配置文件里用 copy 这个 directive),我司维护 rancher 的团队为 4 人,但这个团队不仅仅维护 rancher,还有不少内部系统开发类、研发类的工作。


2020-04-12 20:43908

评论

发布
暂无评论
发现更多内容

【稳定性平台】GOREPLAY流量录制回放实战

得物技术

golang 得物 GOREPLAY 稳定性平台

2021年Java面试题抢先看,够全!,java技术支持面试题

Java 程序员 后端

深度学习平台百度飞桨亮相"十三五"科技创新成就展

百度大脑

人工智能 百度

2021年最新Java后端学习路线,适用于所有想要踏入Java行业的初学者!

Java 程序员 后端

2021年金三银四最新美团、字节、阿里,阿里巴巴java面试流程

Java 程序员 后端

2021终于拿到阿里Java后端岗offer!只因我做了这个决定

Java 程序员 后端

2021-07-22 Java练习题,kafka数据存储原理

Java 程序员 后端

2021首次分享面试阿里P6心得:1000字超全面试题答案解析

Java 程序员 后端

更务实的联想,要做钢筋铁骨的边缘智能

脑极体

2020年,阿里最新的java程序员面试题目含答案带你吊打面试官

Java 程序员 后端

2020金九银十面试总结,大厂Java面试必会知识点,基础+底层+算法+数据库

Java 程序员 后端

2021年备战金三银四:死磕“源码,百度网盘搜索引擎java

Java 程序员 后端

2020年京东Java研发岗社招面经(面试经历+真题总结,java编程教程视频下载

Java 程序员 后端

2021先定个小目标?搞清楚MyCat分片的两种拆分方法和分片规则!

Java 程序员 后端

2021全网最新、最全面“互联网大厂面试题库2400页,nginx反向代理负载均衡原理

Java 程序员 后端

用四个问题引导员工解决问题

石云升

职场经验 管理经验 10月月更

2021最新华为面经分享:Java高分面试指南(25分类1000题50w字解析

Java 程序员 后端

2021最新Java岗面试清单:15个技术模块(程序员必备,威力加强版

Java 程序员 后端

2021最新美团面经分享:999页Java程序员面试清单(下载量已突破30W

Java 程序员 后端

2020金九银十面试总结,大厂Java面试必会知识点(1),java基础入门第二版第二章答案

Java 程序员 后端

2021备战金三银四血拼一波算法:字节+百度,Java进阶推荐

Java 程序员 后端

2021年京东、拼多多、腾讯,javaspringboot面试题

Java 程序员 后端

2021年面试会更难?Java必备209道真题,这份清单助你轻松入阿里

Java 程序员 后端

2021金三银四程序员必备:“基础-中级-高级,几种线程安全的Map解析

Java 程序员 后端

2021年Java面试题抢先看,够全!中篇,java基础程序

Java 程序员 后端

2021年去一线大厂面试先过SSM框架源码这一关!,你还看不明白?

Java 程序员 后端

2021最强面试笔记非它莫属:3000字Java面试核心手册(大厂必备

Java 程序员 后端

2020最新阿里巴巴必问的200个面试题以及答案,助你斩获阿里offer

Java 程序员 后端

2021年九月最新Java面试必背八股文,338道最新大厂架构面试题

Java 程序员 后端

2021最新 SSM(Spring+Spring MVC,java分布式系统面试题

Java 程序员 后端

太难为我了,三战阿里,拿下27K*16offer(附七面面经)

Java 程序员 架构 面试 后端

AI独角兽商汤科技的内部服务容器化历程_文化 & 方法_Rancher_InfoQ精选文章