️This article has been over 3 years since the last update.
SonarQube是来自瑞士的代码检查工具,除了用来检查项目,它本身也是开源的,源码(代码结构/技术文档等)也必然是值得一读。
通过阅读源码,我们可以学到
- 一款顶尖复杂的软件项目的结构与模块如何划分
- 一个复杂前端的React实现
- 如何混合使用ES与数据库
在阅读本文前,众所周知源码分析非常耗费时间,重复一下源码分析方法论
- 能够充分使用过此项目,并阅读文档,这一步主要掌握上下文与术语
- 分析项目的pom依赖,并全部过一遍
- 找到免编译的路由断点与关键日志位置
- 充分使用FindUsage快速Jump
预先准备
由于编译SonarQube非常繁琐耗时,我建议提前编译好的二进制文件
下载源码
- 下载/手动编译二进制文件,解压运行,并确保已经有数据
本地编译流程
1 | ./build.sh |
然后解压并右键启动
1 | java -jar sonar-application/build/distributions/sonarqube-8.9-SNAPSHOT/lib/sonar-application-8.9-SNAPSHOT.jar |
目录解析
如下是安装目录
1 | bin:壳而已,Docker容器启动不需要 |
配置断点
在IDEA中配置Remote断点,并在源码中如下位置打下断点
1 | org.sonar.ce.app.CeServer#start |
然后再次重新运行SonarQube,如果Ok的话断点就断上了
SonarQube组件构成
服务端(ServerSide)
在服务端(org.sonar.application.App)启动时,会依此启动如下Process(通过ps aux|grep sonar分析)
1 | org.sonar.application.SchedulerImpl#tryToStartAll |
它们的作用分别是
- ElasticSearch: 内嵌的ElasticSearch,版本为7,未修改任何jar包,类似HBase的rowkey的作用
- Compute Engine(CE): 计算引擎,通过解析计算客户端上传的Zip,显示到前端;定时刷新db到elk
- Web: 本质是数据仓库的前台。主要有用React实现的前端与API代理,通过内嵌的Tomcat实现部署
它们的PID分别如下
1 | p_es=`ps aux|grep Elasticsearch|grep java |awk '{print $2}'` |
通过lsof查看每个服务的监听情况,并在源码中全局搜索
1 | lsof -i -n -P |grep java|grep LISTEN|grep ${p_es} |
客户端(ScannerSide)
源码分析器,在客户机上计算,通过HTTP将ZIP发给WebServer,内部有ClassLoader,可以下载服务端的插件到客户端执行Sensor解析等操作。
1 | # 上传zip |
这里主要涉及到插件开发,可以参考sonar-cxx等优质插件,插件文档很不友好,对编码要求高。
我在一些项目中,涉及到读取第三方report,这时推荐使用Java-grok框架提高开发速度。
组件实现
数据库(demo-only)
内嵌H2直接免密码访问如下JDBC即可
1 | jdbc:h2:tcp://127.0.0.1:9092/sonar |
当然生产时可以自己搭建一个本地的pg库。在Java层使用了MyBatis进行查询。所有的SQL都没有用自增主键,而是用的是类似雪花算法(绑定了Mac地址与timestamp)的UuidFactoryImpl实现的。
这种类似UUID的实现导致无法使用基于主键的二分Join查找,而一定需要基于分词器的ELK作为二级索引缓存(比如先查询issue的ids,再用in去查询files),否则性能会极低。
我使用内嵌的数据库测试发现一个问题:Quality Profiles only supports up to 6.4k build-in rules .
ElasticSearch里存的啥
在SonarQube中,ElasticSearch的配置文件是程序生成的。
入口
1 | org.sonar.application.command.CommandFactory#createEsCommand |
默认通过elastic head等工具访问http://localhost:9001/就可以知道里面存的啥了。其实里面只存储了一些主键的uuid
比如下面存储了issues的主键kee对应下面的_id
1 | { |
此外还有rules也只是用来存储主键,并没有像想象中缓存了很多error/warings的报错。这里的ELK场景类似于提供了一个类似HBase的唯一RowKey(联合主键),是Stream计算后的缓存的缓存,下游DB只用查询in即可,性能更好,这样就算是一堆left join,也都是低计算强度的聚合。
假如你将ELK删除,然后重启可以发现会自动修复,说明本项目中ELK定位只是主键/分页的索引缓存,这个Key承载了太多信息。
在收费版中,Sonarqube支持多个ELK节点,而免费版不支持分离部署。
外置Elasticsearch并不是为了解决Sonarqube的扩容瓶颈问题,而主要是解决单点可用性问题,你放到外部就算天天备份也意义不大。
注意配置要提高Xms与Xmx,默认的512太低;将Elastic的索引文件外挂Docker磁盘,可以提高reindex速度。
How to use external Elasticsearch in Sonarqube
It’s just a hack at your own risk. See details at stackoverflow
注意主节点插件升级后,ELK也会同步导入规则,这个场景没有测试
本文实验如下,下面为ELK的配置
1 | # ELK side, IP=192.168.33.10 |
sonar配置
1 | # sonar side, test only |
执行mock脚本
1 | cat > ./elasticsearch/bin/elasticsearch << EOF |
1 | java -jar lib/sonar-application-8.9.3-SNAPSHOT.jar |
这种将ELK与Sonar分离部署的方案,启动速度实测为15s左右(主要是ce耗费时间)
如果需要reindex,按照官方标准的话,就是先关机sonar,清空elk,再启动sonar通过ce进行reindex
1 | # 重启后相关任务 |
WebServer
它部署了一个嵌入的Tomcat,通过Java(而不是通过Spring的DSL)手动实现添加webApp
1 | org.sonar.server.app.TomcatContexts#addContext |
等Tomcat部署后,将调用PlatformServletContextListener启动Platform(这里并没有使用Spring作为依赖注入,而是使用了picocontainer实现,使用Java编码而没有用注解/XML等DSL进行描述依赖关系),启动API业务
最终部署如下业务
| 部署类型 | ContextPath | ByWho |
|---|---|---|
| API业务 | /api | WebServiceFilter, 类似于Struts |
| React静态文件 | 默认是/, 详见sonar.web.context | web |
| 插件Jar仓库静态文件 | /deploy | data/web/deploy |
所有的API请求均可以通过如下位置进行断点分析到业务中
1 | org.sonar.server.ws.WebServiceEngine#execute |
这样本文的引导作用就达到了,剩下具体业务自行断点分析
我在这里耗费了较多时间,本以为API业务是由Servlet进行处理,没想到居然是通过全手写Filter与Action的方法处理(10年前这种方法很先进),可以看出SonarQube也是有历史债务的,但是它的代码质量经过长期maintain后仍然清晰。
注意项目中的
StaticResourcesServlet已经事实上废弃,因为已经没有static文件夹了(但是插件可能会使用一些js/css)
CE如何录入计算结果?
CE的启动流程
1 | // main入口 |
最核心的是启动了如下一堆定时器
1 | // CeQueueInitializer |
最简单的两分钟清理一次任务: cleaningScheduler
- resetTasksWithUnknownWorkerUUIDs: 将未知UUID的Work的任务配置为PENDING,这里单机版对应的是重装后删除旧的work任务;分布式版本对应hazelcast中不健康的Work任务自动下线,可以搜索
update ce_queue set。这里不会删除Pending长时间的任务。 - cancelWornOuts: delete所有Pending的任务
假如你部署了多台Sonarqube社区版连接同一台机器,那么Work的UUID由于是基于时间生成,那么将导致相互reset
总流程如下
1 | Scanner(client-side)-(HTTP POST Zip)->Web-(MQ)->CE |
首先在Scanner通过Maven等工具在Jenkins等平台(占用这些平台的计算资源)计算出项目的各种分析报告,然后Scanner调用Web中如下接口
1 | // http://localhost:9000/api/ce/submit?projectKey=xxx&projectName=yyy |
WebServer将原始RAW文件录入ce_task_input,接着通过消息队列(DB Based),可以类比为Celery
1 | org.sonar.ce.queue.CeQueueImpl#submit(org.sonar.ce.queue.CeTaskSubmit) |
CE侧通过线程池每隔两秒轮询查询任务
1 | org.sonar.ce.taskprocessor.CeWorkerImpl#call |
最终任务将通过路由到如下位置,执行数据仓库录入等任务(CeTaskProcessor)
1 | org.sonar.ce.task.step.ComputationStepExecutor#executeStep |
最终匹配的消费者为ExtractReportStep,进行异步计算并存储到数据仓库
CeTaskTypes.REPORT
ReportTaskProcessor: CeTaskTypes.REPORT
此外还有一个任务
IssueSyncTaskProcessor: CeTaskTypes.BRANCH_ISSUE_SYNC 当重启时,会触发此服务。首先检查sonar.internal.es.disableIndexes,然后检查ELK数据是否损坏,并重新reindex到ELK
- IgnoreOrphanBranchStep: 通过ce_task的uuid -> project_branches中是否有数据
- IndexIssuesStep: 通过ce_task的uuid -> project_branches.need_issue_sync判断是否需要同步,然后将 issues/rules/components 的join结果缓存到ELK
思考:为何不直接用MyBatis的缓存注解框架,而是用了ELK?
疑问解答
SonarQube如何实现存储源码?
通过访问表file_sources中的BINARY_DATA与protobuf/LZ4压缩实现存储,它与File通过FILE_UUID进行关联
1 | org.sonar.server.source.ws.LinesAction#handle |
你在前台查询的issues等信息,实际上就是数据库多张表join查出来的,SQL优化的好,就算没缓存也很快。
SonarQube与Markdown
通过基于正则表达式的规则引擎实现,这个做的比较简单,没有实现AST
1 | org.sonar.channel.ChannelDispatcher#consume |
SonarQube的React如何实现
前台使用了React与JSX实现业务,使用Webpack进行打包,使用WebPackDevServer作为API代理,前端通过如下启动外壳业务,打包脚本见server/sonar-web/config/webpack.config.js
1 | # 启动业务(使用NodeJS提供Server) |
SonarQube如何分析代码AST?
这里采用插件实现,比如Java在这里可以找到,分析后将转为通用格式发给Web进行处理
如果项目组需要定制Custom Rule,就可以通过访问onMethodInvocationFound实现自己的规则
当然更推荐用第三方的report解析,比如sonar-cxx插件,这种就只用解析xml而不用写parser了,这种生态成本反而更低一些
高可用思路
首先不要忘记停机也没啥大不了的,它本质上只属于支持业务。
收费版分析
首先要明确,收费版(最贵的DC版)即使通过分散元数据实现了扩容(Enterprise-scale),也有downtime的问题:Cluster downtime is required for SonarQube upgrades or plugin installations.
基于文档分析如下,不含LB与DB的话,按照200M的issue需求,需要5个节点
- 其中2个是jar包业务(4U/16G)
- 3个是Jar包里的ElasticSearch(8U/32G)
这套没什么缺点,除了贵以外。
再次强调,分布式,横向扩容,高可用三者间没有任何覆盖关系。
社区版分析
Sonarqube是单机程序,存在downtime故障。假如想不掏钱实现多机器部署的高可用,那么注意
思路一:仍然单节点,降低重启downtime时间
- Elastic:内置的全部屏蔽,外移,方案同上。
- 减少插件与插件中的规则,因为启动时会加载规则到DB中,可以删掉不用的css/c#等插件
思路二:仍然单节点,思路一 + 报告回传加入持久化
新增一套生产消费者(类似Azure Stroage Gateway or Azure Queue Storage),将应用侧基于DB的队列换为云服务。
- 队列:需要实现file upload queue或者kafka+s3,自己轮训检查后台Pending为空后,从云队列中获取。
思路三:多机Web服务高可用(不推荐)
事实上这套方案意义本身不大,公司级的内部项目,甚至用户粘性没必要做到这么高的运维级别,无状态的Web应用本身CURD很难挂掉,而有状态的CE却难以实现多机部署,导致ROI很低。
需要解决的问题
- Elastic:内置的全部屏蔽,外移,方案同上。
- CE: 有状态有定时任务的消费者,需要屏蔽只留一台。分布式方案基本上就靠买了,否则自己做还不如搭建多台url-hash切片用。
- Web:理论上是无状态/生产者,可以多机部署,本来就是个壳而已。假如插件更新,仍然要全部停机。但是内部黑盒是否有另外坑,尚不明确。
- 多机的JWT-session问题: 需要用Delegating Authentication/SSO来实现,只用LDAP是不够的。可以配合使用Traefik等网关的filter实现。
- 升级插件重启中的rules表与缓存刷新:尚未明确是否有lock等暗坑
这种高成本导致还不如用子域名部署多台,因为就算你部署成功,部分场景测试ok。但是一旦升级,你没法证明过程是可信的。
写入场景
- 通过API写入门禁规则等信息
- 通过上传报告写入issue信息
高可用且横向扩展
- 分离部署:不要同时启动三个服务。
- 前台Web高可用:解决Session问题就是无状态了,可以用ExternalGroupsProvider实现
- CE高可用:要实现独立部署,不能和web程序混在一起,否则web重启时无法持久化
其它
总结
对个人来说,分析这个平台是有点后悔的----这套代码本身就是数十年的祖传项目,耗费太多时间了,我从2017年时不时就研究下,但是收益却只有苦劳,几次改版后可能就白分析了。
从功利的角度来说,这类CICD的方向太窄,而且基本上只有BAT玩得起这种定制,它本身的LGPL协议也并不友好。
个人看法
- 不要去折腾内部实现高可用等技术细节,ROI太低,重启也没啥;加钱买买买也可以。
- 将时间投入在静态检查插件,Wrapper封装等业务中,产生效率价值
- 分析本身的溢价很低,投入在算法等基础组件上更好。这么搞面试太吃亏了,假如我是雇主,也不会考虑支付sonar插件外的溢价。说得不好听,可能连能面试你的人都找不到。
高可用组网探索
- Do at your own risk without any community or business support.
- No scaling implemented: only one compute engine work
- Upgrade plugins will require a full restart
- Consume a lot of time.
Prerequisites node
- clusters(K8S or nomad) or docker exec directly.
- Traefik or Consul side-car or the other application gateway(DaemonSet) * N
- External Elastic7 Service * 1
- External Postgres Service * 1
- External SSO/SAML Service * 1
- SonarQube LTS(with computer engine only) * 1
- SonarQube LTS(with elastic/ce removed) * N
Architecture
安全加固
参考这里的泄漏事故
- Dockerfile: 基于LTS镜像
- 不采用root的gosu启动,卸载无用的apt包
- 对sonar.properties关键信息使用自带AES工具加密,整体配置chmod400/nobody
- 移除/审计无用的自带插件,降低攻击面
- 系统配置
- 不暴露地址到外网
- 关闭默认共享项目;配置全局模版,不共享
sonar-user - 集成SSO/LDAP登陆,并考虑实现群组定时push,基于群组而非用户管理权限
- admin只开启view与admin权限,不开启view source code功能,安装后专人回收账号