java應用診斷和在線debug利器bistoury介紹與在K8S環境使用
- 2020 年 4 月 9 日
- 筆記
Bistoury介紹
Bistoury
是去哪兒網開源的一個對應用透明,無侵入的java應用診斷工具,用於提升開發人員的診斷效率和能力,可以讓開發人員無需登錄機器或修改系統,就可以從日誌、記憶體、執行緒、類資訊、調試、機器和系統屬性等各個方面對應用進行診斷,提升開發人員診斷問題的效率和能力。
Bistoury
集成了Alibaba開源的arthas和唯品會開源的vjtools,因此arthas和vjtools相關功能都可以在Bistoury
中使用。
Arthas和vjtools通過命令行或類似的方式使用,Bistoury在保留命令行介面的基礎上,還對很多命令提供了圖形化介面,方面用戶使用。
Bistoury
英文解釋是外科手術刀,含義也就不言而喻了。
Screenshots
通過命令行介面查看日誌,使用arthas和vjtools的各項功能
在線debug,在線應用調試神器
執行緒級cpu監控,幫助你掌握執行緒級cpu使用率
在web介面查看JVM運行資訊,以及各種其它資訊
動態給方法添加監控
執行緒dump
Bistoury架構分析
Bistoury核心組件包含agent,proxy,ui:
- agent : 與需要診斷的應用部署到一起,負責具體的診斷命令執行,通過域名連接proxy
- proxy:agent的代理,agent啟動時會通過ws和proxy連接註冊,proxy可以部署多個,推薦使用域名負載
- ui:ui提供圖形化和命令行介面,接收從用戶傳來的命令,傳遞命令給proxy,接收從proxy傳來的結果並展示給用戶。
一次命令執行的數據流向為 ui -> proxy -> agent -> proxy -> ui
具體分析一下:
- proxy 先啟動,將自己地址註冊到zk
- agent通過域名訪問proxy,隨機分配到一個proxy,在proxy註冊自己
- UI 訪問一個具體的應用時,通過zk拿到所有的proxy,然後依次檢查app對應的agent是否在該proxy,如果在,web網頁連接這個proxy
- web上輸入一個命令:web->proxy->agent->proxy->ui
具體參見 https://github.com/qunarcorp/bistoury/blob/master/docs/cn/design/design.md
bistoury原理分析: https://www.jianshu.com/p/f7202e490156
總結下就是使用類似skywalking那樣的agent技術,來監測和協助運行在JVM上的程式。
Bistoury快速開始
官方有一個快速開始文檔: https://github.com/qunarcorp/bistoury/blob/master/docs/cn/quick_start.md
可以下載release包快速啟動,就可以體驗了。
首先我們將快速啟動包 bistoury-quick-start.tar.gz 拷貝到想要安裝的位置。
然後解壓啟動包:
tar -zxvf bistoury-quick-start.tar.gz cd bistoury
最後是啟動 Bistoury,因為 Bistoury 會用到 jstack 等操作,為了保證所有功能可用,需要使用和待診斷 JAVA 應用相同的用戶啟動。
假設應用進程 id 為 1024
- 如果應用以本人用戶啟動,可以直接運行
./quick_start.sh -p 1024 start
- 如果應用以其它帳號啟動,比如 tomcat,需要指定一下用戶然後運行
sudo -u tomcat ./quick_start.sh -p 1024 start
- 停止運行
./quick_start.sh stop
Bistoury 在docker運行
官方的git倉庫里,有一個docker分支,翻閱後找到相關文檔。
官方的快速啟動命令:
#!/bin/bash #創建網路 echo "start create network" docker network create --subnet=172.19.0.0/16 bistoury #mysql 鏡像 echo "start run mysql image" docker run --name mysql -p 3307:3306 -e MYSQL_ROOT_PASSWORD=root -d -i --net bistoury --ip 172.19.0.7 registry.cn-hangzhou.aliyuncs.com/bistoury/bistoury-db #zk 鏡像 echo "start run zk image" docker run -d -p 2181:2181 -it --net bistoury --ip 172.19.0.2 registry.cn-hangzhou.aliyuncs.com/bistoury/zk:latest sleep 30 #proxy 鏡像 echo "start run proxy module" docker run -d -p 9880:9880 -p 9881:9881 -p 9090:9090 -i --net bistoury --ip 172.19.0.3 registry.cn-hangzhou.aliyuncs.com/bistoury/bistoury-proxy --real-ip $1 --zk-address 172.19.0.2:2181 --proxy-jdbc-url jdbc:mysql://172.19.0.7:3306/bistoury #ui 鏡像 echo "start run ui module" docker run -p 9091:9091 -it -d --net bistoury --ip 172.19.0.4 registry.cn-hangzhou.aliyuncs.com/bistoury/bistoury-ui --zk-address 172.19.0.2:2181 --ui-jdbc-url jdbc:mysql://172.19.0.7:3306/bistoury #boot 鏡像 echo "start run demo application" docker run -it -d -p 8686:8686 -i --net bistoury --ip 172.19.0.5 registry.cn-hangzhou.aliyuncs.com/bistoury/bistoury-demo --proxy-host $1:9090 docker run -it -d -p 8687:8686 -i --net bistoury --ip 172.19.0.6 registry.cn-hangzhou.aliyuncs.com/bistoury/bistoury-demo --proxy-host $1:9090
上面的命令不能直接運行,$1
是需要替換成當前伺服器IP,然後再運行就OK了。
Bistoury 在生產環境運行
官方推薦部署方式:
-
ui 獨立部署,推薦部署在多台機器,並提供獨立的域名
-
proxy 獨立部署,推薦部署在多台機器,並提供獨立的域名
-
agent 需要和應用部署在同一台機器上。推薦在測試環境全環境自動部署,線上環境提供單機一鍵部署,以及應用下所有機器一鍵部署
-
獨立的應用中心,管理所有功能內部應用和機器資訊,這是一個和 Bistoury 相獨立的系統,Bistoury 從中拿到不斷更新的應用和機器資訊
這裡有個關鍵的點,應用中心,Bistoury內置了一個簡單的應用中心,Bistoury里程式碼對應bistoury-application,ui和proxy都通過這個工程獲取應用資訊,官方默認實現了一個mysql版本的:
使用mysql的缺點是,你需要ui介面裏手動維護應用以及應用的伺服器,做個demo還OK,生產環境肯定不行。更優雅的方式是,用戶系統應該在啟動時自動註冊到註冊中心上,彙報自己的應用、機器資訊(ip、域名等)、埠等資訊。當然這個對大部分微服務架構來說,註冊中心是標配的,因此實現一套bistoury-application-api介面即可。
bistoury-application-k8s(Bistoury on K8S)
我們項目組所有的應用都部署在K8S環境,因此要實現一個bistoury-application-k8s
。
拷貝bistoury-application-mysql
項目,建立bistoury-application-k8s
簡單對應下:
- 一個應用對應一個deployment,對應一個application
- 一個deployment里有n個pod,對應applicationServer
所以,我們只需要調用調用K8S API 獲取deployment和pod即可。
首先引入相關jar包:
<dependency> <groupId>io.kubernetes</groupId> <artifactId>client-java</artifactId> <version>8.0.0</version> <scope>compile</scope> </dependency>
初始化ApiClient
ApiClient defaultClient = Configuration.getDefaultApiClient(); defaultClient.setBasePath(k8sApiServer); ApiKeyAuth BearerToken = (ApiKeyAuth) defaultClient.getAuthentication("BearerToken"); BearerToken.setApiKey(k8sToken); BearerToken.setApiKeyPrefix("Bearer"); defaultClient.setVerifyingSsl(false);
獲取deployment
區分下是獲取所有namespace,還是獲取指定的namespace
private List<V1Deployment> getDeployments() throws ApiException { AppsV1Api appsV1Api = new AppsV1Api(k8SConfiguration.getApiClient()); return k8SConfiguration.isAllNamespace() ? appsV1Api.listDeploymentForAllNamespaces(false, null, null, null, 0, null, null, 120, false).getItems() : getNamespacesDeployments(k8SConfiguration.getAllowedNamespace()); } List<V1Deployment> getNamespacesDeployments(List<String> namespaces) { AppsV1Api appsV1Api = new AppsV1Api(k8SConfiguration.getApiClient()); List<V1Deployment> deploymentList = new ArrayList<>(); for (String nameSpace : namespaces) { try { deploymentList.addAll(appsV1Api.listNamespacedDeployment(nameSpace, null, null, null, null, null, 0, null, 120, false).getItems()); } catch (ApiException e) { logger.error("get " + nameSpace + "'s deployment error", e); } } return deploymentList; }
轉換為application:
private List<Application> getApplications(List<V1Deployment> applist) { return applist.stream().map(this::getApplication).collect(Collectors.toList()); } private Application getApplication(V1Deployment deployment) { Application application = new Application(); application.setCreateTime(deployment.getMetadata().getCreationTimestamp().toDate()); application.setCreator(deployment.getMetadata().getName()); application.setGroupCode(deployment.getMetadata().getNamespace()); application.setName(deployment.getMetadata().getName()); application.setStatus(1); application.setCode(getAppCode(deployment.getMetadata().getNamespace(), deployment.getMetadata().getName())); return application; }
獲取pod
獲取pod相對麻煩點,需要先獲取到V1Deployment,拿到部署的lableSelector,然後根據lableSelector選擇pod:
public List<AppServer> getAppServerByAppCode(final String appCode) { Preconditions.checkArgument(!Strings.isNullOrEmpty(appCode), "app code cannot be null or empty"); try { V1Deployment deployment = getDeployMent(appCode); String nameSpace = appCode.split(APPCODE_SPLITTER)[0]; Map<String, String> labelMap = Objects.requireNonNull(deployment.getSpec()).getSelector().getMatchLabels(); StringBuilder lableSelector = new StringBuilder(); labelMap.entrySet().stream().forEach(e -> { if (lableSelector.length() > 0) { lableSelector.append(","); } lableSelector.append(e.getKey()).append("=").append(e.getValue()); }); CoreV1Api coreV1Api = new CoreV1Api(k8SConfiguration.getApiClient()); V1PodList podList = coreV1Api.listNamespacedPod(nameSpace, null, false, null, null, lableSelector.toString(), 200, null, 600, false); return podList.getItems().stream().map(pod -> { AppServer server = new AppServer(); server.setAppCode(appCode); server.setHost(pod.getMetadata().getName()); server.setIp(pod.getStatus().getPodIP()); server.setLogDir(k8SConfiguration.getAppLogPath()); server.setAutoJMapHistoEnable(true); server.setAutoJStackEnable(true); server.setPort(8080); return server; }).collect(Collectors.toList()); } catch (ApiException e) { logger.error("get deployment's pod error", e); } return null; }
最後,修改ui和proxy工程,將原來的mysql替換為k8s:
應用引入bistoury agent
這塊相對比較容易:
在需要調試的應用的Dockerfile里增加:
COPY --from=hub.xfyun.cn/abkdev/bistoury-agent:2.0.11 /home/q/bistoury /opt/bistoury
然後修改應用的啟動腳本,在最前面增加:
BISTOURY_APP_LIB_CLASS="org.springframework.web.servlet.DispatcherServlet" # default proxy PROXY="bistoury-bistoury-proxy.incubation:9090" AGENT_JAVA_HOME="/usr/local/openjdk-8/" # env if [[ -n $PROXY_HOST ]]; then PROXY=$PROXY_HOST fi TEMP=`getopt -o : --long proxy-host:,app-class:,agent-java-home: -- "$@"` eval set -- "$TEMP" while true; do case "$1" in --proxy-host ) PROXY="$2"; shift 2 ;; --app-class ) BISTOURY_APP_LIB_CLASS="$2"; shift 2 ;; --agent-java-home ) AGENT_JAVA_HOME="$2"; shift 2 ;; * ) break ;; esac done echo "proxy host: "$PROXY_HOST echo "app class: "$BISTOURY_APP_LIB_CLASS echo "agent java home: "$AGENT_JAVA_HOME
在最後面增加:
APP_PID=`$AGENT_JAVA_HOME/bin/jps -l|awk '{if($2!="sun.tools.jps.Jps"){print $1 ;{exit}} }'` echo "app pid: "$APP_PID /opt/bistoury/agent/bin/bistoury-agent.sh -j $AGENT_JAVA_HOME -p $APP_PID -c $BISTOURY_APP_LIB_CLASS -s $PROXY -f start
集成測試
部署一個測試應用 agent-debug-demo,部署到jx namespace:
{ "kind": "Deployment", "apiVersion": "extensions/v1beta1", "metadata": { "name": "agent-debug-demo", "namespace": "jx", "annotations": { "deployment.kubernetes.io/revision": "2" } }, "spec": { "replicas": 1, "selector": { "matchLabels": { "app": "agent-debug-demo", "draft": "draft-app" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { "app": "agent-debug-demo", "draft": "draft-app" } }, "spec": { "containers": [ { "name": "springboot-rest-demo", "image": "hub.xxx.cn/abkdev/springboot-rest-demo:dev-113", "ports": [ { "containerPort": 8080, "protocol": "TCP" } ], "env": [ { "name": "SPRING_PROFILES_ACTIVE", "value": "dev" }, { "name": "PROXY_HOST", "value": "$PROXY_HOST:9090" } ], "resources": {}, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "imagePullPolicy": "IfNotPresent" } ], "restartPolicy": "Always", "terminationGracePeriodSeconds": 10, "dnsPolicy": "ClusterFirst", "securityContext": {}, "schedulerName": "default-scheduler" } }, "strategy": { "type": "RollingUpdate", "rollingUpdate": { "maxUnavailable": 1, "maxSurge": 1 } }, "revisionHistoryLimit": 2147483647, "progressDeadlineSeconds": 2147483647 }, "status": { "observedGeneration": 2, "replicas": 1, "updatedReplicas": 1, "unavailableReplicas": 1, "conditions": [ { "type": "Available", "status": "True", "lastUpdateTime": "2020-04-09T01:32:42Z", "lastTransitionTime": "2020-04-09T01:32:42Z", "reason": "MinimumReplicasAvailable", "message": "Deployment has minimum availability." } ] } }
部署後:
打開ui,查看:
應用名稱顯示為: namespace名稱-部署名稱
在線調試:
先選擇應用:
點擊Debug,然後選擇需要調試的類,
測試工程源程式碼為:
@SpringBootApplication @Controller public class RestPrometheusApplication { @Autowired private MeterRegistry registry; @Autowired private Environment env; @GetMapping(path = "/", produces = "application/json") @ResponseBody public Map<String, Object> landingPage() { Counter.builder("mymetric").tag("foo", "bar").register(registry).increment(); String profile = "default"; if(env.getActiveProfiles().length > 0){ profile = env.getActiveProfiles()[0]; } return singletonMap("hello", ""+ profile); } public static void main(String[] args) { SpringApplication.run(RestPrometheusApplication.class, args); } }
因此,我們輸入RestPrometheusApplication篩選:
然後點擊調試,可以看到,反編譯出來了源程式碼:
在landingPage最後一行加一個端點,然後點擊添加端點,最後訪問該POD對應的服務,該pod對應的ip是170.22.149.37,因此我們訪問:
curl http://170.22.149.37:8080 {"hello":"dev"}
再回到UI,可以看到成員變數,局部變數和調用堆棧等資訊。
well down!
作者:Jadepeng
出處:jqpeng的技術記事本–http://www.cnblogs.com/xiaoqi
您的支援是對部落客最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。