1. 程式人生 > >java應用診斷和線上debug利器bistoury介紹與在K8S環境使用

java應用診斷和線上debug利器bistoury介紹與在K8S環境使用


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
您的支援是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。