韓東
摘要:傳統(tǒng)軟件交付模式偏向于手動(dòng),軟件從源碼到編譯、打包、測(cè)試、交付生產(chǎn)經(jīng)歷不同的形態(tài)和系統(tǒng)環(huán)境。手動(dòng)模式效率較低,且不同環(huán)境存在差異,容易引起錯(cuò)誤導(dǎo)致軟件部署失敗。設(shè)計(jì)基于Kubernetes容器集群環(huán)境的持續(xù)交付平臺(tái),利用容器技術(shù)的隔離特性屏蔽程序運(yùn)行環(huán)境的差異,利用git、docker、Kubernetes的C/S架構(gòu)模式,集成每個(gè)平臺(tái)的客戶端及相關(guān)構(gòu)建工具,使程序在拉取項(xiàng)目、編譯、部署過程中擺脫命令行模式,只需在持續(xù)交付平臺(tái)中通過表單方式配置每個(gè)過程中所需的各項(xiàng)參數(shù)即可,將軟件交付過程中所有操作集中到統(tǒng)一平臺(tái)上,實(shí)現(xiàn)自動(dòng)化交付。系統(tǒng)測(cè)試結(jié)果表明,持續(xù)交付平臺(tái)能夠自動(dòng)完成軟件交付工作,打通了軟件交付各個(gè)環(huán)節(jié)的數(shù)據(jù)流和業(yè)務(wù)流。
關(guān)鍵詞:容器集群;持續(xù)交付;Docker;Kubernetes
DOI:10. 11907/rjdk. 201144
中圖分類號(hào):TP319文獻(xiàn)標(biāo)識(shí)碼:A 文章編號(hào):1672-7800(2020)010-0186-05
Abstract:The traditional software delivery model tends to be manual, and the software experiences different forms and system environments from source code to compilation, packaging, testing, and production. Manual mode is inefficient, and differences in different environments can easily cause unexpected errors and cause software deployment failure. A continuous delivery platform is designed based on the Kubernetes container cluster environment, the isolation characteristics of container technology are used to shield the differences in program operating environments, and the C / S architecture model of git, docker, and Kubernetes is used to integrate the clients of each platform and related construction tools so that the program can get rid of the command-line mode in the process of pulling the project, compiling, and deploying. It only needs to configure the various parameters required in each process through the form in the continuous delivery platform to centralize all operations in the software delivery process so that a unified platform for automated delivery can be made. The system test results show that the continuous delivery platform can automatically complete the software delivery work and open up the data flow and business flow in all links of software delivery.
Key Words: container cluster; continuous delivery; Docker; Kubernetes
0 引言
持續(xù)交付是DevOps中的一個(gè)重要方法,主要研究如何通過自動(dòng)化方法使得軟件部署與交付變得更加便利[1]。DevOps強(qiáng)調(diào)應(yīng)持續(xù)集成與部署,做到每天進(jìn)行價(jià)值交付,測(cè)試人員在軟件開發(fā)過程中就參與進(jìn)來,每天進(jìn)行集成測(cè)試,運(yùn)維人員進(jìn)行持續(xù)部署。DevOps強(qiáng)調(diào)通過自動(dòng)化“軟件交付”和“架構(gòu)變更”流程,使構(gòu)建、測(cè)試、發(fā)布軟件更加快捷、頻繁及可靠[2-4]。矛盾在于軟件部署通常是復(fù)雜的,經(jīng)常出現(xiàn)測(cè)試環(huán)境、生產(chǎn)環(huán)境與開發(fā)環(huán)境不一致的情況,從而導(dǎo)致軟件交付失敗。開發(fā)人員希望每天交付新功能,而運(yùn)維人員期望系統(tǒng)保持穩(wěn)定,這似乎是不可調(diào)和的矛盾[5-6]。在傳統(tǒng)模式下,軟件交付花費(fèi)成本過高,導(dǎo)致DevOps期望的每天都能集成、測(cè)試與部署不能真正實(shí)現(xiàn)。
面對(duì)持續(xù)交付中的矛盾,相關(guān)學(xué)者與從業(yè)人員給出多種解決方法,如周振興[7]提出通過 GitHub以及 IBM Urban Code Deploy(UCD)工具進(jìn)行自動(dòng)化部署實(shí)現(xiàn)持續(xù)交付,并通過虛擬化技術(shù) Docker 對(duì)系統(tǒng)進(jìn)行水平擴(kuò)展;丘暉[8]利用Jenkins的Pipeline功能實(shí)現(xiàn)Docker容器環(huán)境下的持續(xù)交付;郭雪[9]提出基于神經(jīng)網(wǎng)絡(luò)的自調(diào)節(jié)持續(xù)集成與交付框架,利用神經(jīng)網(wǎng)絡(luò)的自學(xué)習(xí)和自調(diào)節(jié)優(yōu)勢(shì)為開發(fā)者預(yù)測(cè)與計(jì)劃最佳的下一次構(gòu)建時(shí)機(jī),從而幫助開發(fā)者合理平衡開發(fā)與集成;金澤鋒等[10]認(rèn)為軟件交付時(shí)應(yīng)注重價(jià)值交付,提出面向完整價(jià)值交付的文檔DevOps,并認(rèn)為產(chǎn)品文檔應(yīng)與軟件同時(shí)交付,避免出現(xiàn)軟件版本與文檔不匹配的問題。
本文提出基于Kubernetes容器集群環(huán)境的持續(xù)交付平臺(tái),利用Docker容器的隔離特性屏蔽程序不同運(yùn)行環(huán)境的差異性,引入Kubernetes容器集群及應(yīng)用編排技術(shù)實(shí)現(xiàn)自動(dòng)化交付。Kubernetes能夠應(yīng)對(duì)復(fù)雜的軟件架構(gòu)和應(yīng)用場(chǎng)景,在DevOps理念被普遍接受的背景下,Docker、Kubernetes在軟件交付中已得到廣泛應(yīng)用[11-12]。目前使用形式大多為:開發(fā)人員將編譯完成的程序包發(fā)送給運(yùn)維人員,由運(yùn)維人員根據(jù)需要使用命令行方式在運(yùn)行Docker引擎的服務(wù)器上生成Docker鏡像,再使用Kubernetes的kubectl命令行工具,將容器部署到容器集群環(huán)境中。整個(gè)過程偏向于手動(dòng)模式,各平臺(tái)之間的數(shù)據(jù)流斷裂。本文通過分析Docker、Kubernetes及gitlab各平臺(tái)系統(tǒng)架構(gòu),得出三者都是基于C/S架構(gòu)模式,可利用各平臺(tái)開源的客戶端,集成到持續(xù)交付平臺(tái)中,在統(tǒng)一的平臺(tái)上集成所有操作,以表單方式配置各流程中的參數(shù),避免使用命令行方式,實(shí)現(xiàn)交付流程的自動(dòng)化。
1 持續(xù)交付平臺(tái)設(shè)計(jì)
持續(xù)交付平臺(tái)旨在管理一個(gè)軟件項(xiàng)目從開發(fā)到交付生產(chǎn)等一系列生命周期,從靜態(tài)代碼開始到編譯與打包,再到將程序包構(gòu)建成Docker鏡像。Docker鏡像如果只存在于開發(fā)環(huán)境的機(jī)器中,則不便于共享與分發(fā)[13],還需將鏡像推送至鏡像倉庫中。最后將構(gòu)建好的鏡像按照軟件架構(gòu)設(shè)計(jì)部署在容器集群中。在整個(gè)過程中,軟件項(xiàng)目從可編輯的源代碼到二進(jìn)制程序包,再到容器鏡像,都需要借助相關(guān)平臺(tái)和工具。例如,使用github作為代碼版本控制工具[14],而Docker鏡像是通過Docker引擎構(gòu)建的[15]。持續(xù)交付平臺(tái)本身不是各項(xiàng)具體工作的實(shí)施者,而是交付流程的創(chuàng)建者,以及各平臺(tái)的統(tǒng)一組織者。持續(xù)交付平臺(tái)通過圖形化界面的方式進(jìn)行操作,以表單方式配置各流程中的參數(shù)。
對(duì)于企業(yè)而言,首先要搭建一套基礎(chǔ)設(shè)施服務(wù)。受企業(yè)內(nèi)部規(guī)定的限制,軟件項(xiàng)目可能只在企業(yè)內(nèi)部網(wǎng)絡(luò)使用,并不想公開在互聯(lián)網(wǎng)上,例如企業(yè)不希望自己的項(xiàng)目源代碼托管在github上,也不希望自己的鏡像發(fā)布在Docker hub中,所以不能使用一些互聯(lián)網(wǎng)的公開服務(wù),而需要企業(yè)自己在內(nèi)部網(wǎng)絡(luò)構(gòu)建一套基礎(chǔ)設(shè)施服務(wù)。本文使用gitlab作為代碼版本控制工具,以及harbor作為Docker鏡像Registry服務(wù),搭建Docker引擎服務(wù)與Kubernetes服務(wù)集群。
持續(xù)交付流程是整個(gè)交付平臺(tái)的核心內(nèi)容,基于各基礎(chǔ)平臺(tái)的持續(xù)交付平臺(tái)交付流程如圖1所示。
具體流程包括:
(1)從gitlab上拉取指定項(xiàng)目的源碼,并進(jìn)行編譯與打包。
(2)根據(jù)項(xiàng)目的Dockerfile文件,向Docker平臺(tái)發(fā)出構(gòu)建鏡像命令,Docker引擎會(huì)根據(jù)項(xiàng)目的Dockerfile進(jìn)行鏡像構(gòu)建。
(3)鏡像構(gòu)建完成后,驅(qū)動(dòng)Docker引擎將鏡像推送至指定鏡像倉庫中。
(4)根據(jù)項(xiàng)目架構(gòu)設(shè)計(jì),命令Kubernetes創(chuàng)建Deployment資源,指定相關(guān)鏡像和Pod副本數(shù),部署至容器集群中。
(5)還需命令Kubernetes創(chuàng)建Service資源,用于服務(wù)發(fā)現(xiàn)。此時(shí)整個(gè)軟件項(xiàng)目完成交付,并提供訪問。
2 持續(xù)交付平臺(tái)實(shí)現(xiàn)
持續(xù)交付平臺(tái)實(shí)現(xiàn)的重點(diǎn)內(nèi)容在于如何在平臺(tái)上操作各個(gè)基礎(chǔ)服務(wù)平臺(tái),完成編譯打包、鏡像構(gòu)建,以及對(duì)推送至容器集群中的各個(gè)資源進(jìn)行創(chuàng)建與管理。幸運(yùn)的是,各平臺(tái)都提供了不同語言版本的客戶端,并且開源。持續(xù)交付平臺(tái)需要集成這些客戶端,使用這些客戶端提供的API進(jìn)行平臺(tái)化操作。其中,如何使用這些API實(shí)現(xiàn)編碼是一個(gè)關(guān)鍵問題。
2.1 客戶端選擇
持續(xù)交付平臺(tái)總體上使用Java語言加以實(shí)現(xiàn),并使用Maven工具進(jìn)行項(xiàng)目構(gòu)建與管理,因此需要選擇各平臺(tái)Java版本的客戶端。各平臺(tái)客戶端Maven依賴詳情如表1所示。
2.2 編程實(shí)現(xiàn)
在編碼實(shí)現(xiàn)環(huán)節(jié),省略了一般性通用功能的編碼實(shí)現(xiàn),如平臺(tái)注冊(cè)/登錄等,而重點(diǎn)聚焦持續(xù)交付流程中核心功能的編碼實(shí)現(xiàn)。持續(xù)交付平臺(tái)需要與各基礎(chǔ)平臺(tái)進(jìn)行交互,編碼重點(diǎn)在于如何利用集成的各平臺(tái)客戶端提供的API進(jìn)行交互實(shí)現(xiàn)。
(1)將項(xiàng)目源碼克隆至本地。持續(xù)交付平臺(tái)與gitlab進(jìn)行交互,需要傳遞代碼倉庫的URL和倉庫用戶名、密碼以及保存至本地的目錄。編碼實(shí)現(xiàn)如下:
public static void cloneGitRepository(GitRepository gitRepository, String localPath) throws Exception {
CloneCommand cc = Git.cloneRepository().setURI(gitRepository.getUrl());
cc.setCredentialsProvider(
new UsernamePasswordCredentialsProvider(gitRepository.getUsername(),
gitRepository.getPassword()));
cc.setDirectory(new File(localPath))。call();
}
(2)創(chuàng)建Docker鏡像。根據(jù)Dockerfile文件利用編譯完成的程序包構(gòu)建Docker鏡像[16],需要在操作系統(tǒng)的環(huán)境變量中配置Docker服務(wù)所在地址,如:DOCKER_HOST=tcp://192.168.18.150:2375。Docker客戶端讀取DOCKER_HOST的值與Docker服務(wù)建立連接。編碼實(shí)現(xiàn)如下:
public String buildImage(String dockerFileDir, String tag) throws Exception {
final DockerClient dockerClient = DefaultDockerClient.fromEnv().build();
File dockerFile = new File(dockerFileDir);
Path dockerFilePath = dockerFile.toPath();
DockerClient.BuildParam buildParam = new DockerClient.BuildParam(“t”, tag);
return dockerClient.build(dockerFilePath, buildParam);
}
(3)推送鏡像至Harbor鏡像倉庫。持續(xù)交付平臺(tái)與Docker平臺(tái)交互,將Docker鏡像推送至harbor鏡像倉庫中。一般來說,企業(yè)內(nèi)部使用的是私有倉庫,需要提供倉庫的用戶名和密碼。編碼實(shí)現(xiàn)如下:
public void pushImage(String username, String pw, String image) throws Exception {
final DockerClient dockerClient = DefaultDockerClient.fromEnv().build();
final RegistryAuth registryAuth = RegistryAuth.create(username, pw, null,
HarborServerAddress, null, null);
dockerClient.push(image, registryAuth);
}
(4)創(chuàng)建Deployment資源,實(shí)現(xiàn)應(yīng)用程序部署。持續(xù)交付平臺(tái)與Kubernetes進(jìn)行交互,創(chuàng)建Deployment資源實(shí)例,由Deployment指揮 Kubernetes 創(chuàng)建與更新應(yīng)用程序?qū)嵗齕17]。創(chuàng)建 Deployment 后,Kubernetes master 將應(yīng)用程序?qū)嵗{(diào)度到集群中的各個(gè)節(jié)點(diǎn)上[18]。首先需要?jiǎng)?chuàng)建Kubernetes客戶端,默認(rèn)讀取當(dāng)前系統(tǒng)用戶文件夾下的.kube/config文件。該文件是創(chuàng)建Kubernetes集群時(shí)生成的配置文件, config文件中描述了Kubernetes master節(jié)點(diǎn)地址信息,以及其它認(rèn)證信息,然后與Kubernetesmaster節(jié)點(diǎn)建立連接。需要指定應(yīng)用程序容器鏡像以及要運(yùn)行Pod的副本數(shù),對(duì)于私有倉庫,需要指定倉庫用戶名、密碼。編碼實(shí)現(xiàn)如下:
public void newDeployment(String deploymentName, String ns, int rs, Map
Config config = new ConfigBuilder().build();
KubernetesClient client = new DefaultKubernetesClient(config);
Deployment deployment = new DeploymentBuilder()。build();
deployment.setKind(KubernetesConstraint.KUBERNETES_DEPLOYMENT);
ObjectMeta deploymentMeta = new ObjectMeta();
deploymentMeta.setNamespace(ns);
deploymentMeta.setName(deploymentName);
deploymentMeta.setLabels(labels);
deployment.setMetadata(deploymentMeta);
DeploymentSpec spec = new DeploymentSpec();
spec.setReplicas(rs);
LabelSelector labelSelector = new LabelSelector();
labelSelector.setMatchLabels(podSelector);
spec.setSelector(labelSelector);
PodTemplateSpec podTemplateSpec = newPodTemplate();
spec.setTemplate(podTemplateSpec);
deployment.setSpec(spec);
client.apps().deployments().create(deployment);
}
(5)創(chuàng)建Service資源,實(shí)現(xiàn)服務(wù)暴露與發(fā)現(xiàn)。此時(shí), Kubernetes僅為Pod創(chuàng)建了集群內(nèi)部的虛擬IP,只有集群內(nèi)部才能訪問[19]。Service資源可以暴露程序運(yùn)行的服務(wù),并且具有負(fù)載均衡功能。創(chuàng)建Service資源需要指定Service的name屬性,并通過Service選擇器與Pod標(biāo)簽進(jìn)行綁定。編碼實(shí)現(xiàn)如下:
public void newService(String serviceName, String ns, Map
Config config = new ConfigBuilder().build();
KubernetesClient client = new DefaultKubernetesClient(config);
Service service = new Service();
service.setKind(KubernetesConstraint.KUBERNETES_SERVICE);
ObjectMeta serviceMeta = new ObjectMeta();
serviceMeta.setName(serviceName);
serviceMeta.setNamespace(ns);
serviceMeta.setLabels(labels);
ServiceSpec serviceSpec = new ServiceSpec();
serviceSpec.setType(“NodePort”);
serviceSpec.setSelector(selector);
service.setSpec(serviceSpec);
service.setMetadata(serviceMeta);
client.services().create(service);
}
3 案例測(cè)試
通過創(chuàng)建一個(gè)springboot微服務(wù)程序驗(yàn)證持續(xù)交付平臺(tái)功能是否達(dá)到預(yù)期。測(cè)試程序能夠讀取主機(jī)名,以驗(yàn)證負(fù)載均衡功能是否實(shí)現(xiàn)。主要測(cè)試能否正確構(gòu)建Docker鏡像并推送至Harbor鏡像倉庫中;能否通過持續(xù)交付平臺(tái)在Kubernetes中部署該程序,并創(chuàng)建副本;能否通過持續(xù)交付平臺(tái)正確創(chuàng)建服務(wù),實(shí)現(xiàn)從集群外部網(wǎng)絡(luò)進(jìn)行訪問。
(1)鏡像構(gòu)建與推送測(cè)試。構(gòu)建Docker鏡像主要使用Dockerfile文件,Dockerfile文件中明確了Docker鏡像創(chuàng)建步驟[20]。測(cè)試程序的Dockerfile如下所示:
# 基礎(chǔ)鏡像
FROM openjdk:8-jdk-alpine
# 對(duì)應(yīng)pom.xml文件中dockerfile-maven-plugin插件buildArgs配置項(xiàng)JAR_FILE的值
ARG JAR_FILE=target/*.jar
# 將打包完成后的jar文件復(fù)制到/opt目錄下
COPY ${JAR_FILE} /opt/app.jar
# 啟動(dòng)容器時(shí)執(zhí)行
ENTRYPOINT [“java”,“-Djava.security.egd=file:/dev/./urandom”,“-jar”,“/opt/app.jar”]
# 使用端口80
EXPOSE 80
使用持續(xù)交付平臺(tái)將鏡像打上標(biāo)簽:hub.devops.com.helloworld/hello-app:v1,并推送至Harbor鏡像倉庫中。推送成功后,Habror平臺(tái)顯示如圖2所示。
(2)部署測(cè)試。通過持續(xù)交付平臺(tái)創(chuàng)建Deployment資源,資源名稱為springboot-app-depolyment,鏡像為hub.devops.com/helloworld/hello-app:v1,標(biāo)簽為app=helloworld,Pod副本數(shù)為4。資源創(chuàng)建成功后,通過命令查看Kubernetes集群中的Deployment資源狀態(tài)如圖3所示。
圖3顯示成功創(chuàng)建并運(yùn)行4個(gè)副本,查看Pod狀態(tài)如圖4所示。圖中顯示4個(gè)Pod正在運(yùn)行。
(3)測(cè)試service資源能否將服務(wù)暴露出去。持續(xù)交付平臺(tái)創(chuàng)建service資源,類型為NodePort,節(jié)點(diǎn)端口為30715,目標(biāo)Pod內(nèi)部端口為80,需要作用于Pod的標(biāo)簽為app=helloworld。使用命令查看創(chuàng)建結(jié)果如圖5所示。
使用Linux curl命令進(jìn)行服務(wù)訪問,curl每次只訪問192.168.18.22:30715,結(jié)果顯示,返回的主機(jī)名每4次重復(fù)1次,說明service資源正常工作。每次訪問服務(wù)時(shí),service使用輪詢算法進(jìn)行負(fù)載均衡,如圖6所示。
4 結(jié)語
本文利用Kubernetes容器集群技術(shù)整合各基礎(chǔ)服務(wù)平臺(tái)客戶端,構(gòu)建自動(dòng)化的持續(xù)交付平臺(tái),打通了軟件交付各環(huán)節(jié)數(shù)據(jù)流和業(yè)務(wù)流。然而,本文對(duì)持續(xù)交付平臺(tái)的研究仍存在不足之處,例如本文只研究了基于Java語言的構(gòu)建工具,而在實(shí)際生產(chǎn)環(huán)境中有多種開發(fā)語言和技術(shù)架構(gòu),還需進(jìn)一步研究基于其它語言與技術(shù)架構(gòu)的構(gòu)建工具,以應(yīng)對(duì)更加復(fù)雜的應(yīng)用場(chǎng)景。持續(xù)交付平臺(tái)關(guān)注軟件交付環(huán)節(jié),因此面向軟件完整生命周期,對(duì)從需求提出到測(cè)試運(yùn)維進(jìn)行全流程管理的DevOps平臺(tái)將是未來研究的重點(diǎn)方向,以進(jìn)一步將DevOps理念落地,打破開發(fā)/運(yùn)維的隔閡之墻。
參考文獻(xiàn):
[1] 付大亮. 拉動(dòng)DevOps持續(xù)交付的三匹馬——快速交付實(shí)踐總結(jié)與思考[J]. 金融電子化,2018(3):58-60.
[2] 高棟,王殿勝,張思琪,等. DevOps平臺(tái)建設(shè)分析[J]. 中國科技信息,2019,10(24):39-40.
[3] 劉博涵,張賀,董黎明. DevOps中國調(diào)查研究[J]. 軟件學(xué)報(bào),2019,30(10):3206-3226.
[4] 牛曉玲,吳蕾. DevOps發(fā)展現(xiàn)狀研究[J]. 電信網(wǎng)技術(shù),2017(10):48-51.
[5] 李超,花磊,宋云奎. OpsFlow:一種面向DevOps的應(yīng)用自動(dòng)化部署引擎 [J]. 計(jì)算機(jī)與數(shù)字工程,2019,47(1):190-194.
[6] 小棗菌. DevOps到底是什么意思[EB/OL]. https://zhuanlan.zhihu.com/p/91371659.
[7] 周振興. 基于 Docker 和持續(xù)交付的項(xiàng)目管理系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)[D]. 大連:大連理工大學(xué),2016.
[8] 丘暉. 基于容器的持續(xù)集成和部署方法研究[J]. 廣東通信技術(shù),2017(10):62-66.
[9] 郭雪. 基于神經(jīng)網(wǎng)絡(luò)的過程自調(diào)節(jié)持續(xù)集成工具設(shè)計(jì)與實(shí)現(xiàn)[D]. 南京:南京大學(xué),2019.
[10] 金澤鋒,張佑文,葉文華,等. 面向完整價(jià)值交付的文檔DevOps應(yīng)用研究[J]. 軟件學(xué)報(bào),2019,30(10):3127-3147.
[11] 張文林. 持續(xù)交付及其在大型項(xiàng)目中的應(yīng)用[J]. 軟件導(dǎo)刊,2017,16(10):159-161.
[12] 梁惠惠. 對(duì)軟件開發(fā)模式變遷的研究[J]. 現(xiàn)代信息科技,2019,3(22):1-8.
[13] MARKO L. Kubernetes in action[M]. 北京:電子工業(yè)出版社,2019.
[14] LEN B. The software architect and DevOps[J]. IEEE Software,2017,14(4):8-10.
[15] 邊俊峰. 基于Docker的資源調(diào)度及應(yīng)用容器集群管理系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)[D]. 濟(jì)南:山東大學(xué),2017.
[16] 楊保華,戴王劍,曹亞侖. Docker技術(shù)入門與實(shí)踐[M]. 北京:機(jī)械工業(yè)出版社,2018.
[17] 馬征,繆凱,張廣溫. 淺析 Kubernetes 容器虛擬化技術(shù)[J]. 應(yīng)用技術(shù),2019,10(2):63-64.
[18] ERIK D. The path to DevOps[J]. IEEE Software,2018(2):71-75.
[19] 鄭冰. 基于Kubernetes的企業(yè)級(jí)容器云平臺(tái)設(shè)計(jì)[J]. 數(shù)字技術(shù)與應(yīng)用,2019,37(6):138-141.
[20] 翁湦元,單杏花,閻志遠(yuǎn),等. 基于Kubernetes的容器云平臺(tái)設(shè)計(jì)與實(shí)踐[J]. 鐵路計(jì)算機(jī)應(yīng)用,2019,28(12):49-53.
(責(zé)任編輯:黃 ?。?/p>