使用fabric8操作k8s

文章目錄

  • 一、引入fabric包
  • 二、認證
    • 1、使用config文件認證
    • 2、使用oauthtoken認證
  • 三、pod的查詢和遍歷
  • 四、命名空間的創建和刪除
  • 五、deployment的創建和刪除
    • 部分參數說明
      • 1、resourceRequirements
      • 2、containerPorts
      • 3、envVarList
      • 4、volumeMounts和volumeList
      • 5、nodeAffinity
  • 六、單個pod的創建和刪除
  • 七、DaemonSet的創建
  • 七、給node打標簽

一、引入fabric包

<dependency><groupId>io.fabric8</groupId><artifactId>kubernetes-client</artifactId><version>5.10.2</version>
</dependency>

二、認證

認證十分簡單,只要拿到認證的config信息后使用以下方式即可。

KubernetesClient client = new DefaultKubernetesClient(config);

當然了,為了方便使用,client最好是存入數據庫后再放在緩存中去維護。且需要對接多個k8s集群時,需要多個KubernetesClient,因此最好是在緩存中維護一個集群編碼和client的對應關系。
那么認證的config信息要怎么拿到呢?通常有使用config文件和oauthtoken認證兩種方式。

當然了,在初始時可以對這個client進行校驗,測試連通性是否有問題,如果校驗通過再在后面對它進行操作。

try {NonNamespaceOperation<Namespace, NamespaceList, Resource<Namespace>> namespaces = kubernetesClient.namespaces();if (namespaces != null) {namespaces.withName("default").get();}
} catch (Exception e) {throw new XxxException(Xxxxxx);
} finally {kubernetesClient.close();
}

1、使用config文件認證

config文件在管理節點的/root/.kube/目錄下面,在頁面上傳后,我們后端拿到的比如說是一個fileUploadReqBO下的byte[] file;

String kubeConfig = null;
Config config = null;
try {kubeConfig = new String(fileUploadReqBO.getFile(), Charsets.UTF_8.toString());config = Config.fromKubeconfig(kubeConfig);
} catch (Exception e) {throw new XxxException(xxxx, e.getMessage());
}

2、使用oauthtoken認證

獲取oauthtoken需要有admin權限的serviceaccount,如果沒有的話那么就自己手動創建一個。
創建serviceaccount賬戶,這里我們就叫test-admin:

kubectl create serviceaccount test-admin -n kube-system

給予admin權限:

kubectl create clusterrolebinding my-service-account-admin --clusterrole=cluster-admin --serviceaccount=kube-system:test-admin

執行以下命令

kubectl get secret -n kube-system|grep admin

找到返回結果中以test-admin-token-開頭的內容,使用以下命令

kubectl describe secret test-admin-token-XXX -n kube-system

就可以獲取到token了
獲取到后可使用kubectl auth can-i create deployments --as=system:serviceaccount:kube-system:test-admin --token= 判斷是否有管理員權限 yes有 no沒有
那么這時假設我們能拿到一個masterUrl,例如 https://10.20.66.152:6443(kube-apiserver一般來說默認端口為6443)以及token。
就可以這樣獲取到config:

Config config = new ConfigBuilder().withTrustCerts(true).build();
config.setMasterUrl(masterUrl);
config.setOauthToken(oauthToken);

三、pod的查詢和遍歷

查詢所有pod:

//已獲取KubernetesClient:KubernetesClient client = new DefaultKubernetesClient(config);
PodList podList = client.pods().list();

根據命名空間查詢:

PodList podList = client.pods().inNamespace(K8sGenericConstant.K8S_NAMESPACE_ENGINE_SERVER).list();

遍歷pod:

if (podList != null && podList.getItems() != null) {for(Pod pod : podList.getItems()){//pod名稱String podName = pod.getMetadata().getName();//pod所在節點名稱String nodeName = pod.getSpec().getNodeName();//pod標簽Map<String, String> labels = pod.getMetadata().getLabels();//命名空間String ns = pod.getMetadata().getNamespace();//狀態pod.getStatus().getContainerStatuses();pod.getStatus().getReason();List<PodCondition> podConditions = pod.getStatus().getConditions();if (!CollectionUtils.isEmpty(podConditions)) {PodCondition podCondition = podConditions.get(0);reason = podCondition.getReason() + ":" + podCondition.getMessage();}}

四、命名空間的創建和刪除

創建

NonNamespaceOperation<Namespace, NamespaceList, Resource<Namespace>> namespaces = client.namespaces();
if (namespaces == null) {return null;
}
String name = "test-ns";
Map<String, String> labels = Maps.newHashMap();
labels.put("testlabel", "testvalue");
Namespace ns = new NamespaceBuilder().withNewMetadata().withName(name).addToLabels(labels).endMetadata().build();
ns = namespaces.createOrReplace(ns);

刪除

NonNamespaceOperation<Namespace, NamespaceList, Resource<Namespace>> namespaces = client.namespaces();
if (namespaces == null) {return null;
}
namespaces.withName(name).delete();

五、deployment的創建和刪除

刪除:

//注意這里deployment需要先查詢出Deployment類型,而不只是名稱
client.apps().deployments().inNamespace(namespace).delete(deployment);
client.apps().deployments().inNamespace(namespace).withName(deploymentname).delete();

創建:

Deployment deployment = new DeploymentBuilder().withNewMetadata().withName(podName).endMetadata().withNewSpec().withNewSelector().addToMatchLabels(matchLabels).endSelector().withReplicas(1).withNewTemplate().withNewMetadata().withLabels(matchLabels).withNamespace(namespace).withAnnotations(annotations).endMetadata().withNewSpec().addNewContainer().withName(podName).withImage(imageUrl).withImagePullPolicy(K8sImagePullPolicyEnum.IF_NOT_PRESENT.getValue()).withResources(resourceRequirements).withPorts(containerPorts).withEnv(envVarList).withVolumeMounts(volumeMounts).withCommand(commandList).withArgs(argList).endContainer().withVolumes(volumeList).withNewAffinity().withNodeAffinity(nodeAffinity).endAffinity().withNodeSelector(nodeSelector).endSpec().endTemplate().endSpec().build();client.apps().deployments().inNamespace(namespace).create(deployment);

部分參數說明

其中的參數比如podName、namespace、podName、imageUrl是String類型,commandList、argList為List<String>類型,但也有不少需要提前構造的參數,比如matchLabels、annotations、nodeSelector是Map<String,String>的類型,又比如以下幾個示例:

1、resourceRequirements

ResourceRequirements resourceRequirements = new ResourceRequirements();
Map<String, Quantity> limits = new HashMap<>();
limits.put("cpu", new Quantity("2000m"));
limits.put("memory", new Quantity("20480Mi"));
limits.put("nvidia.kubernetes.io/gpu", new Quantity("1"));
Map<String, Quantity> requests = new HashMap<>();
requests.put("cpu", new Quantity("1000m"));
requests.put("memory", new Quantity("10240Mi"));
requests.put("nvidia.kubernetes.io/gpu", new Quantity("1"));
resourceRequirements.setRequests(requests);
resourceRequirements.setLimits(limits);

注意這里的limits.put()后面的key要和describe node獲取的一致。比如這里的gpu用的是nvidia.kubernetes.io/gpu,如果是其他廠商的或者映射出來的不一致,則要和環境中保持一致。實際使用中經常做成可配置/傳參的,由于這里只是一個示例,因此寫死了。

Capacity:cpu:                8ephemeral-storage:  308468608Kihugepages-1Gi:      0hugepages-2Mi:      0memory:             32771060Kinvidia.kubernetes.io/gpu:     1pods:               200

2、containerPorts

containerPorts需要的類型是List<ContainerPort>
也就是如下圖所示:

public synchronized List<ContainerPort> buildContainerPorts() {LOGGER.info("ports={}", ports);List<ContainerPort> containerPortList = Lists.newArrayList();//實際使用時需作為入參傳入List<ContainerPortBO>,這里作為示范直接寫死ContainerPort port = new ContainerPort();port.setHostPort(32111);port.setName("web-port");port.setProtocol("TCP");port.setContainerPort(32111);containerPortList.add(port);//假設這里我們已經獲得了一個containerPortListcontainerPortList = containerPortList.stream().filter(p -> p.getHostPort() != null && p.getContainerPort() != null).collect(Collectors.toList());if (CollectionUtils.isEmpty(containerPortList)) {return null;}// 如果由上層直接指定端口的話,這里直接return containerPortList即可//但當需要我們自己去分配端口時 需要盡量避免端口沖突,因此做了以下處理(并不完全能避免,但至少如果某個節點跑多個pod,不會只能跑一個其他的都在pending)// 1.查詢每個POD占用的端口PodList podList = K8sClientTool.getKubernetesClient().pods().list();Set<Integer> excludeNodePortList = Sets.newHashSet();if (podList != null && podList.getItems() != null) {for (Pod pod : podList.getItems()) {List<Integer> portList = pod.getSpec().getContainers().stream().flatMap(m ->m.getPorts().stream().filter(p -> p.getHostPort() != null).map(ContainerPort::getHostPort)).collect(Collectors.toList());excludeNodePortList.addAll(portList);}}// 2.獲取組件安裝機器的端口,一般aid安裝在K8S集群的主節點上,這樣可以規避掉主要的端口try {String result = SshTool.doExecute("netstat -nlpt  | grep -Po '\\d+(?=.+)' | sort -rn | xargs -n1");if (StringUtils.isNotEmpty(result)) {excludeNodePortList.addAll(Arrays.stream(result.split("\n")).map(s -> Integer.parseInt(s.trim())).collect(Collectors.toList()));}} catch (Exception e) {throw new ComputeResourceException(AidServerErrorCode.ERR_DEVICE_SSH_CONNECT);}// 3.解決容器端口的占用和沖突問題,這里需要解決并發的問題,加一個鎖來處理List<Pair<Integer, Long>> needRemovePortPairList = Lists.newArrayList();// 4.先加入配置文件中要排除的端口excludeNodePortList.addAll(Arrays.stream(excludeNodePorts.split(",")).map(s -> Integer.parseInt(s.trim())).collect(Collectors.toList()));// 5.再加入歷史分配出去的端口,這些端口有可能沒有真正的分配出去,但是需要緩存,避免同時出現2個要分配的端口excludeNodePortList.addAll(excludeNodePortPairList.stream().map(pair -> {if (pair.getRight() < (System.currentTimeMillis() - DEFAULT_TIME_TO_LIVE)) {return pair.getLeft();}needRemovePortPairList.add(pair);return null;}).filter(p -> p != null).collect(Collectors.toSet()));// 6.清理掉過期的緩存端口excludeNodePortPairList.removeAll(needRemovePortPairList);LOGGER.info("containerPortList={}, excludeNodePortList={}", containerPortList, excludeNodePortList);containerPortList.stream().forEach(c -> {// 優先使用分配的hostPort,不滿足再隨機分配Integer hostPort = c.getHostPort();while (excludeNodePortList.contains(hostPort)) {hostPort = RandomUtils.nextInt(minNodePort, maxNodePort);}excludeNodePortList.add(hostPort);excludeNodePortPairList.add(Pair.of(hostPort, System.currentTimeMillis()));if (StringUtils.isNotEmpty(c.getName())) {c.setName(c.getName().toLowerCase().replaceAll("_", "-"));if (c.getName().length() > 15) {c.setName(c.getName().substring(0, 15));}}c.setHostPort(hostPort);});LOGGER.info("containerPortList={}", containerPortList);return containerPortList;
}

3、envVarList

List<EnvVar> envVarList = Lists.newArrayList();
EnvVar envVar = new EnvVar();
envVar.setName("TEST_ENV_KEY");
envVar.setValue("TEST_ENV_VALUE");
envVarList.add(envVar);

4、volumeMounts和volumeList

假設參數以List<Map<String, String>>形式傳入,例如:
“volumeMounts”:[{“name”:“test-name”,“mountPath”:“/home/test”,“hostPath”:“/home/test”}]

volumeMounts:

public List<VolumeMount> buildVolumeMounts(List<Map<String, String>> volumeMountMapList) {List<VolumeMount> volumeMounts = Lists.newArrayList();if (!CollectionUtils.isEmpty(volumeMountMapList)) {for (Map<String, String> map : volumeMountMapList) {volumeMounts.add(TypeTool.castToBean(map, VolumeMount.class));}}
//    VolumeMount testVolumeMount = new VolumeMount();
//    testVolumeMount.setName("test-name");
//    testVolumeMount.setMountPath("/home/test");
//    volumeMounts.add(testVolumeMount); volumeMounts.add(testVolumeMount);return volumeMounts;
}

volumeList:

public List<Volume> buildVolumes(List<VolumeMount> volumeMounts, List<Map<String, String>> volumeMountMapList) {return volumeMounts.stream().map(m -> {Volume volume = new Volume();volume.setName(m.getName());String path = m.getMountPath();if (!CollectionUtils.isEmpty(volumeMountMapList)) {Optional<Map<String, String>> optional = volumeMountMapList.stream().filter(p -> m.getName().equals(p.get("name"))).findFirst();if (optional.isPresent()) {Map<String, String> volumeMap = optional.get();if (volumeMap.containsKey("hostPath")) {path = optional.get().get("hostPath");}}}HostPathVolumeSource hostPath = new HostPathVolumeSource();hostPath.setPath(path);volume.setHostPath(hostPath);return volume;}).collect(Collectors.toList());
}

5、nodeAffinity

List<NodeSelectorRequirement> matchExpressions = Lists.newArrayList();matchExpressions.add(new NodeSelectorRequirementBuilder().withKey("nvidia.kubernetes.io/gpu")//GpuTypeEnum.toContainerValues():List<String>.withOperator("In").withValues(GpuTypeEnum.toContainerValues()).build());NodeAffinity nodeAffinity = new NodeAffinityBuilder().withNewRequiredDuringSchedulingIgnoredDuringExecution().withNodeSelectorTerms(new NodeSelectorTermBuilder().withMatchExpressions(matchExpressions).build()).endRequiredDuringSchedulingIgnoredDuringExecution().build();

六、單個pod的創建和刪除

刪除:

client.pods().inNamespace(namespace).delete(pod);
client.pods().inNamespace(namespace).withName(podname).delete();

創建:

Pod podToCreate = new PodBuilder().withNewMetadata().withName(podName).withNamespace(namespace).withLabels(labels).withAnnotations(annotations).endMetadata().withNewSpec().addNewContainer().withName(podName).withImage(imageUrl).withImagePullPolicy("IfNotPresent").withResources(resourceRequirements).withPorts(containerPorts).withEnv(envVarList).withVolumeMounts(volumeMounts).withCommand(commandList).withArgs(argList).endContainer().withNodeSelector(nodeSelector).withRestartPolicy("OnFailure").withVolumes(volumeList)//如果需要容忍污點.addNewToleration().withEffect("NoSchedule").withOperator("Exists").endToleration()//節點選擇策略.withNewAffinity().withNodeAffinity(nodeAffinity).endAffinity().and().build();
Pod pod = null;
try {pod = client.pods().create(podToCreate);
} catch (Exception e) {}

這里需要用到的參數和deployment的差不多,就不贅述了。

七、DaemonSet的創建

和deployment的創建大致一致,只是使用的是client.apps().daemonSets()
以及和上面的示例相比沒有replicas,這里就不再做說明了。

七、給node打標簽

//先查出所需node
NodeList nodeList = client.nodes().list();
//篩選出需要的node
Optional<Node> optionalNode = nodeList.getItems().stream().filter(e -> e.getMetadata().getUid().equals(indexCode)).findFirst();
if (!optionalNode.isPresent()) {throw new XxxException();
}
// 4. 處理node標簽
Node node = optionalNode.get();
//獲取原有標簽
Map<String, String> labels = node.getMetadata().getLabels();
//加入新的標簽
labels.put("xxx","xxx")
//設置標簽
node.getMetadata().setLabels(labels);
//保存
client.nodes().createOrReplace(node);

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/39242.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/39242.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/39242.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

「51媒體」企業舉行新聞發布會,如何邀請媒體到場報道

傳媒如春雨&#xff0c;潤物細無聲&#xff0c;大家好&#xff0c;我是51媒體網胡老師。 媒體宣傳加速季&#xff0c;100萬補貼享不停&#xff0c;一手媒體資源&#xff0c;全國100城線下落地執行。詳情請聯系胡老師。 企業舉行新聞發布會時&#xff0c;邀請媒體到場報道是一個…

MySQL常用操作命令大全

文章目錄 一、連接與斷開數據庫1.1 連接數據庫1.2 選擇數據庫1.3 斷開數據庫 二、數據庫操作2.1 創建數據庫2.2 查看數據庫列表2.3 刪除數據庫 三、表操作3.1 創建表3.2 查看表結構3.3 修改表結構3.3.1 添加列3.3.2 刪除列3.3.3 修改列數據類型 3.4 刪除表 四、數據操作4.1 插入…

day62--若依框架(基礎應用篇)

若依搭建 若依版本 官方 若依官方針對不同開發需求提供了多個版本的框架&#xff0c;每個版本都有其獨特的特點和適用場景&#xff1a; 前后端混合版本&#xff1a;RuoYi結合了SpringBoot和Bootstrap的前端開發框架&#xff0c;適合快速構建傳統的Web應用程序&#xff0c;其…

【Arm技術日:為AI終端準備了哪些新基石?】

過去一年&#xff0c;移動終端設備的長足進步令人贊嘆&#xff0c;例如人工智能 (AI) 從手機到筆記本電腦的巨大創新&#xff0c;并誕生了“新一代 AI 手機”和 AIPC。據IDC預測&#xff0c;2024年全球新一代AI手機的出貨量將達到1.7億部&#xff0c;占智能手機市場總量的近15%…

Qt加載SVG矢量圖片,放大縮小圖片質量不發生變化。

前言&#xff1a; 首先簡單描述下SVG: SVG 意為可縮放矢量圖形&#xff08;Scalable Vector Graphics&#xff09;。 SVG 使用 XML 格式定義圖像。 給界面或者按鈕上顯示一個圖標或背景圖片&#xff0c;日常使用.png格式的文件完全夠用&#xff0c;但是有些使用場景需要把圖…

QChartView顯示實時更新的溫度曲線圖(二)

文章目錄 參考圖說明1. 項目結構2. TempChartView.pro3. main.cpp4. TemperatureSeries.qml5. main.qml詳細說明 參考圖 說明 Qt Charts 提供了一系列使用圖表功能的簡單方法。它使用Qt Graphics View Framework 圖形視圖框架&#xff0c;因此可以很容易集成到用戶界面。可以使…

基于小波分析的紋理和顏色反射對稱性檢測(MATLAB R2018A)

對稱物體在自然圖像和合成圖像中普遍存在。作為對稱物體最重要的全局特征之一&#xff0c;對稱性檢測長期以來都是計算機視覺領域的研究熱點&#xff0c;并在圖片的語義提取、圖像語義理解以及情感識別等任務上具有廣泛的應用。對稱物體的檢測技術&#xff0c;就是將圖片中所蘊…

【前端】HTML+CSS復習記錄【3】

文章目錄 前言一、from&#xff08;表單&#xff09;二、style屬性1、標簽中直接定義&#xff08;內聯樣式&#xff09;2、定義在head中3、外部鏈接引用 四、 class 選擇器系列文章目錄 前言 長時間未使用HTML編程&#xff0c;前端知識感覺忘得差不多了。通過梳理知識點&#…

qq文件傳輸助手在哪里?詳細圖文教程告訴你(2024新版)

QQ作為一款功能強大的社交軟件&#xff0c;不僅提供了聊天、語音、視頻等多種通訊方式&#xff0c;還內置了文件傳輸助手這一實用工具。通過文件傳輸助手&#xff0c;用戶可以在不同設備之間輕松傳輸文件&#xff0c;實現跨平臺的便捷操作。 那么&#xff0c;qq文件傳輸助手在…

機器之心:自動化與機器人技術詳解

目錄 引言 自動化與機器人技術的定義 發展歷程 深化應用 工業領域 醫療領域 農業領域 服務領域 其他領域 面臨的挑戰與未來展望 一、成本問題 二、技術瓶頸 三、安全性和倫理問題 四、就業問題 未來趨勢 一、智能化和自主化&#xff1a;機器人技術的核心驅動力…

tmux 命令

這篇是另一篇內容的前置知識。因為項目部署測試需要&#xff0c;向公司申請了一個虛擬機做服務器用。以下是回溯的命令&#xff0c;多了解了解&#xff0c;拓寬知識面吧。PS&#xff1a;本人小白一個&#xff0c;知識淺顯&#xff0c;勿噴。 tmux 常用快捷鍵 tmux 提供了一系…

【C語言】常見的數據排序算法

目錄 一、概述 二、常見的排序算法 2.1 冒泡排序 2.1.1 定義 2.1.2 C語言實現 2.2 快速排序 2.2.1 定義 2.2.2 C語言實現 2.3 插入排序 2.3.1 定義 2.3.2 C語言實現 2.4 希爾排序 2.4.1 定義 2.4.2 C語言實現 2.5 歸并排序 2.5.1 定義 2.5.2 C語言實現 2.6 基…

【@AutoWired和@Resource的區別】

AutoWired和Resource的區別 這兩個我們在項目中&#xff0c;經常去使用。很少有人知道他們有什么區別。下面我們將從 來源依賴查找順序支持的參數依賴注入的用法支持 這四個方面來說明他們倆個的區別 來源 Autowired: 這是Spring框架自帶的注解&#xff0c;用于實現自動依…

絕區零 Mac 下載安裝詳細教程(MacOS IPA 砸殼包 playCover 完美運行)

絕區零 7.4 號開始公測&#xff0c;但剛剛就可以開始下載了&#xff0c;我也是第一時間就迫不及待的安裝到了我的 Mac 電腦上&#xff0c;感興趣的朋友可以跟我一起安裝試試 我這里是通過 playCover 的形式在 Mac 上安裝運行的&#xff0c;根據之前原神的經驗所以這次還是同樣…

惠海 H6912 升壓恒流芯片IC 支持2.6-40V升12V24V36V48V60V100V 10A 攝影燈 太陽能燈 UV燈 殺菌燈

1.產品描述 H6912是一款外圍電路簡潔的寬調光比升壓調光LED恒流驅動器&#xff0c;可適用于2.6-40V輸入 電壓范圍的LED恒流照明領域。H6912可以實現高精度的恒流效果&#xff0c;輸出電流恒流精度≤士3%&#xff0c;電壓工作范圍為2.6-40V.可以輕松滿足鋰電池及中低壓的應用需…

Python中的爬蟲實戰:貓眼電影爬蟲

隨著互聯網技術的快速發展&#xff0c;網絡上的信息量越來越龐大。貓眼電影作為國內領先的電影數據平臺&#xff0c;為用戶提供了全面的電影信息服務。本文將介紹如何利用python編寫簡單的貓眼電影爬蟲&#xff0c;獲取電影相關數據。 爬蟲概述 爬蟲&#xff0c;即網絡爬蟲&a…

x264 編碼器 common.h 文件中結構體詳細介紹

x264_slice_header_t 定義:typedef struct {x264_sps_t *sps;x264_pps_t *pps;int i_type;int i_first_mb;int i_last_mb;int i_pps_id;int i_frame_num

嵌入式Linux系統編程 — 6.1 信號的基本概念

目錄 1 信號的概念和作用 1.1 什么是信號 1.2 信號的目的 1.3 信號如何處理 2 信號的分類 2.1 可靠信號與不可靠信號 2.2 實時信號與非實時信號 3 常見信號與默認行為 3.1 信號本質上是 int 類型數字編號 3.2 常見信號 1 信號的概念和作用 1.1 什么是信號 信號是一…

艾體寶干貨 | 解析Redis企業版的多租戶技術

在多租戶架構中&#xff0c;一個軟件實例為多個不同的用戶組&#xff08;或“租戶”&#xff09;提供服務。每個租戶的數據都被安全地隔離&#xff0c;確保它們對其他租戶不可見且無法訪問。可以將其想象為一棟公寓大樓&#xff0c;每個人都住在共享建筑中獨立且隔離的單元中。…

Java 商城后臺管理系統

### 構建一個健壯的商城后臺管理系統 使用Java Spring Boot框架和MySQL數據庫&#xff0c;逐步構建一個健壯、安全、高效的商城后臺管理系統。本文涵蓋用戶管理、商品管理、訂單管理、分類管理、權限控制、日志記錄、分頁和排序、文件上傳、緩存以及國際化。 --- #### 項目初…