最近打算写一个 macos 翻译软件, 需要用到 ocr 图像识别, 并且因为速度问题, 一开始就考虑使用系统的自带能力来实现.
经过翻阅文档和 chatgpt 拉扯了一下午, 最终成功实现.
代码逻辑为, 接受参数: 图片路径, 然后获取图片, 通过 VNImageRequestHandler
对图片进行文字识别
如下代码可以直接放进一个 ocr.swift
, 然后执行 swiftc -o ocr ocr.swift
, 在执行 ./ocr /Users/yelog/Desktop/3.png
后面为你实际的有文字的图片路经
//// ocr.swift// Fast Translation//// Created by 杨玉杰 on 2023/12/31.//import SwiftUIimport Visionfunc handleDetectedText(request: VNRequest?, error: Error?) { if let error = error { print("ERROR: \(error)") return } guard let results = request?.results, results.count > 0 else { print("No text found") return } for result in results { if let observation = result as? VNRecognizedTextObservation { for text in observation.topCandidates(1) { let string = text.string print("识别: \(string)") } } }}func ocrImage(path: String) { let cgImage = NSImage(byReferencingFile: path)?.ciImage()?.cgImage let requestHandler = VNImageRequestHandler(cgImage: cgImage!) let request = VNRecognizeTextRequest(completionHandler: handleDetectedText) // 设置文本识别的语言为英文 request.recognitionLanguages = ["en"] request.recognitionLevel = .accurate do { try requestHandler.perform([request]) } catch { print("Unable to perform the requests: \(error).") }}extension NSImage { func ciImage() -> CIImage? { guard let data = self.tiffRepresentation, let bitmap = NSBitmapImageRep(data: data) else { return nil } let ci = CIImage(bitmapImageRep: bitmap) return ci }}// 执行函数,从命令行参数中获取图片的地址ocrImage(path: CommandLine.arguments[1])
然后准备待识别的有文字的图片
# 编译 swift 文件swiftc -o ocr ocr.swift# 执行并且传递图片路径参数./ocr /Users/yelog/Desktop/3.png
最近打算着手写一些 macos 的小工具, 如果对 swift
或者 macos
感兴趣的可以关注或评论.
今天同事再升级框架后(spring-cloud 2022.0.4 -> 2023.0.0)(spring-boot 3.1.6 -> 3.2.0)
同时因为 spring-boot 的版本问题, 需要将 maven 升级到 3.6.3+
升级后构建 jar 包和构建镜像都是正常的, 但是发布到测试环境就报错 Caused by: java.lang.ClassNotFoundException: org.springframework.boot.loader.JarLauncher
报错为 JarLauncher 找不到, 检查了 Jenkins
中的打包任务, 发现并没有编译报错, 同事直接使用打包任务中产生的 xx.jar, 可以正常运行.
说明在打包 Docker
镜像前都没有问题, 这时就想起来我们在打包镜像时, 先解压 xx.jar, 然后直接执行 org.springframework.boot.loader.JarLauncher
, 所以很可能是升级后, 启动文件 JarLauncher
的路径变了.
为了验证我们的猜想, 我们得看一下实际容器内的文件结构, 但是这时容器一直报错导致无法启动, 不能直接通过 Rancher
查看文件结构, 我们可以通过文件拷贝的方式来解决, 如下:
# 下载有问题的镜像, 并且创建容器(不启动)docker create -it --name dumy 10.188.132.123:5000/lemes-cloud/lemes-gateway:develop-202312111536 bash# 直接拷贝容器内的我们想要看的目录docker cp dumy:/data .
到本地后, 就可以通过合适的工具查找 JarLauncher
文件, 我这里通过 vim
来寻找, 如下图:
发现比原来多了一层目录 launch
, 所以问题就发生在这里了.
我们在打包脚本 JenkinsCommon.groovy
中根据当前打包的 JDK 版本来判断使用的启动类路径, 如下:
再次打包, 应用正常启动.
由于最近几年使用 vim 的频率越来越高, 所以在 idea 中也大量开始使用 vim 技巧, 在一年多前碰到个问题, 终于在最近解决了。
在 idea 中, 在 normal 模式下, 使用 % 不能在匹配标签(xml/html等) 之间跳转
在 ~/.ideavimrc
中添加如下设置, 重启 idea 即可
set matchit
最近会把一些加的 tips 分享出来,大家有什么建议和问题都可以在评论区讨论.
]]>最近在很多平台上看到 Raycast 的推荐文章, 今天就尝试了一下, 发现确实不错, 完全可以替代我目前对 Alfred 的使用, 甚至很多地方做得更好, 所以本文就是介绍我使用 Raycast 的一些效果(多图预警), 方便那些还没有接触这个软件的人对它有个了解, 如果有插件推荐, 欢迎在评论区进行讨论。
Raycast 是 MacMac 平台独占的效率工具, 主要包含如下功能:
插件扩展功能
除此之外, Raycast提供的在线插件商店, 可以很方便的进行功能扩展
并且支持卸载应用, 找到应用, cmd+k
打开操作菜单, 下拉到最后
推荐使用 Clipboard History
这个扩展,和 Alfred 的一样, 并且有分类,效果如下
设置快捷键 cmd+shift+v
通过 Snippets 可以保存自定义片段, 通过关键字快速查询并输出到光标处, 如常用语、 邮箱、手机号、税号、代码片段等等
创建 Snippets 可以通过搜索 Create Snippet
, 搜索 Snippets 可以通过搜索Search Snippet
查询当前激活应用的所有菜单, 不限层级. 可以通过搜索 Search Menu Items
来查询。
超强的翻译软件, 完美替代我在 Alfred 的 workflow 中配置的有道翻译, 我配置了如下功能
表情包查询,选中回车就复制到剪贴板了, 就可以粘贴到 Discord/QQ/Wechat 斗图了, 非常方便。
关键字查询, 并一键结束进程
查询端口占用的进程, 并支持一键结束进程
查询当前ip
我们在写后端请求的时候, 可能涉及多次 SQL 执行(或其他操作), 当这些请求相互不关联, 在顺序执行时就浪费了时间, 这些不需要先后顺序的操作可以通过多线程进行同时执行, 来加速整个逻辑的执行速度.
既然有了目标和大致思路, 如果有做过前端的小伙伴应该能想起来 Js 里面有个 Promise.all
来解决这个问题, 在 Java 里也有类似功能的类 CompletableFuture
, 它可以实现多线程和线程阻塞, 这样能够保证等待多个线程执行完成后再继续操作.
首先我们先了解一下 CompletableFuture
是干什么, 接下来我们通过简单的示例来介绍他的作用.
long startTime = System.currentTimeMillis();//生成几个任务List<CompletableFuture<String>> futureList = new ArrayList<>();futureList.add(CompletableFuture.supplyAsync(()->{ sleep(4000); System.out.println("任务1 完成"); return "任务1的数据";}));futureList.add(CompletableFuture.supplyAsync(()->{ sleep(2000); System.out.println("任务2 完成"); return "任务2的数据";}));futureList.add(CompletableFuture.supplyAsync(()->{ sleep(3000); System.out.println("任务3 完成"); return "任务3的数据";}));//完成任务CompletableFuture<Void> allTask = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])) .whenComplete((t, e) -> { System.out.println("所有任务都完成了, 返回结果集: " + futureList.stream().map(CompletableFuture::join).collect(Collectors.joining(","))); });// 阻塞主线程allTask.join();System.out.println("main end, cost: " + (System.currentTimeMillis() - startTime));
执行结果
任务2 完成任务3 完成任务1 完成所有任务都完成了, 返回结果集: 任务1的数据,任务2的数据,任务3的数据main end, cost: 4032
结果分析: 我们需要执行3个任务, 3个任务同时执行, 互不影响
whenComplete
方法, 打印任务的返回结果经过上面的测试, 通过 CompletableFuture
已经能够实现我们的预想, 为了操作方便, 我们将封装起来, 便于统一管理
package org.yelog.java.usage.concurrent;import java.util.ArrayList;import java.util.List;import java.util.concurrent.CompletableFuture;import java.util.function.Consumer;import java.util.function.Function;import java.util.function.Predicate;/** * 执行并发任务 * * @author yangyj13 * @date 11/7/22 9:49 PM */public class MultiTask<T> { private List<CompletableFuture<T>> futureList; /** * 添加待执行的任务 * * @param completableFuture 任务 * @return 当前对象 */ public MultiTask<T> addTask(CompletableFuture<T> completableFuture) { if (futureList == null) { futureList = new ArrayList<>(); } futureList.add(completableFuture); return this; } /** * 添加待执行的任务(无返回) * * @param task 任务 * @return 当前对象 */ public MultiTask<T> addTask(Consumer<T> task) { addTask(CompletableFuture.supplyAsync(() -> { task.accept(null); return null; })); return this; } /** * 添加待执行的任务(有返回) * * @param task 任务 * @return 当前对象 */ public MultiTask<T> addTask(Function<Object, T> task) { addTask(CompletableFuture.supplyAsync(() -> task.apply(null))); return this; } /** * 开始执行任务 * * @param callback 当所有任务都完成后触发的回调方法 * @param waitTaskExecuteComplete 是否阻塞主线程 */ private void execute(Consumer<List<T>> callback, Boolean waitTaskExecuteComplete) { CompletableFuture<Void> allFuture = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])) .whenComplete((t, e) -> { if (callback != null) { List<T> objectList = new ArrayList<>(); futureList.forEach((future) -> { objectList.add(future.join()); }); callback.accept(objectList); } }); if (callback != null || waitTaskExecuteComplete == null || waitTaskExecuteComplete) { allFuture.join(); } } /** * 开始执行任务 * 等待所有任务完成(阻塞主线程) */ public void execute() { execute(null, true); } /** * 开始执行任务 * * @param waitTaskExecuteComplete 是否阻塞主线程 */ public void execute(Boolean waitTaskExecuteComplete) { execute(null, waitTaskExecuteComplete); } /** * 开始执行任务 * * @param callback 当所有任务都完成后触发的回调方法 */ public void execute(Consumer<List<T>> callback) { execute(callback, true); }}
那么上一步我们测试的流程转换成工具类后如下
long startTime = System.currentTimeMillis();MultiTask<String> multiTask = new MultiTask<>();multiTask.addTask(t -> { sleep(1000); System.out.println("任务1 完成");}).addTask(t -> { sleep(3000); System.out.println("任务2 完成");}).addTask(CompletableFuture.supplyAsync(()->{ sleep(2000); System.out.println("任务3 完成"); return "任务3的数据";})).execute(resultList->{ System.out.println("all complete: " + resultList);});System.out.println("main end, cost: " + (System.currentTimeMillis() - startTime));
执行两次数据库的操作如下
public interface TestMapper { @Select("select count(*) from test_user where score < 1000 and user_id = #{userId}") int countScoreLess1000(Integer userId); @Select("select count(1) from test_log where success = true and user_id = #{userId}") int countSuccess(Integer userId);}
调用方法:
long start = System.currentTimeMillis();testMapper.countScoreLess1000(userId);long countScoreLess1000End = System.currentTimeMillis();log.info("countScoreLess1000 cost: " + (countScoreLess1000End - start));testMapper.countSuccess(userId);long countSuccessEnd = System.currentTimeMillis();log.info("countSuccess cost: " + (countSuccessEnd - countScoreLess1000End));log.info("all cost: " + (countSuccessEnd - start));
顺序执行的平均时间如下
countScoreLess1000 cost: 368countSuccess cost: 404all cost: 772
当我们应用的上面的工具类后的调用方法
MultiTask multiTask = new MultiTask<>();multiTask.addTask(t -> { testMapper.countScoreLess1000(userId); log.info("countScoreLess1000 cost: " + (System.currentTimeMillis() - start));}).addTask(t -> { testMapper.countSuccess(userId); log.info("countSuccess cost: " + (System.currentTimeMillis() - start));}).execute();log.info("all cost: " + (System.currentTimeMillis() - start));
效果如下
countScoreLess1000 cost: 433countSuccess cost: 463all cost: 464
可以看到各子任务执行时长是差不多的, 但是总耗时使用多线程后有了明显下降
通过使用 CompletableFuture
实现多线程阻塞执行后, 大幅降低这类请求, 并且当可以异步执行的子任务越多, 效果越明显.
后台框架是基于 spring cloud 的微服务体系, 当开发同学在自己电脑上进行开发工作时, 比如开发订单模块, 除了需要启动订单模块外, 还需要启动网关模块、权限校验模块、公共服务模块等依赖模块, 非常消耗开发同学的本地电脑的资源, 也及其浪费时间.
能不能开发同学本地只需要启动需要开发的模块:订单模块, 其他模块均适用测试环境中正在运行的服务.
既然要实现的目标有了, 我们就开始研究可行性和关键问题
既要在同一个 namespace 下, 又要能够实现不同人访问不同的副本, 很容易想到可以利用灰度发布
来实现:
lemes-env=product
来标识测试环境副本, 用于区分开发环境的微服务测测试环境的微服务假设我们需要开发的 API 的后台服务调用链条如下:
我们需要开发的 API 为 /addMo
, 打算写在 Order
这个微服务里面, 并且他会调用 common
这个微服务的 /getDict
获取一个字典数据, /getDict
是现成的, 不需要开发, 如果是之前的情况, 开发本地至少需要启动5个微服务才能进行调试.
由于测试环境都是通过容器部署的, 那么启动方式就是下面容器中的 CMD
, 我们在其中加入 -Dspring.cloud.nacos.discovery.metadata.lemes-env=product
, 用于区分开发环境的微服务测测试环境的微服务
# 说明:Dockerfile 过程分为两部分。第一次用来解压 jar 包,并不会在目标镜像内产生 history/layer。第二部分将解压内容分 layer 拷贝到目标镜像内# 目的:更新镜像时,只需要传输代码部分,依赖没有变动则不更新,节省发包时的网络传输量# 原理:在第二部分中,每次 copy 就会在目标镜像内产生一层 layer,将依赖和代码分开,# 绝大部分更新都不会动到依赖,所以只需更新代码几十k左右的代码层即可FROM 10.176.66.20:5000/library/amazoncorretto:11.0.11 as builderWORKDIR /buildARG ARTIFACT_IDCOPY target/${ARTIFACT_ID}.jar app.jarRUN java -Djarmode=layertools -jar app.jar extract && rm app.jarFROM 10.176.66.20:5000/library/amazoncorretto:11.0.11LABEL maintainer="yangyj13@lenovo.com"WORKDIR /dataARG ARTIFACT_IDENV ARTIFACT_ID ${ARTIFACT_ID}# 依赖COPY --from=builder /build/dependencies/ ./COPY --from=builder /build/snapshot-dependencies/ ./COPY --from=builder /build/spring-boot-loader/ ./# 应用代码COPY --from=builder /build/application/ ./# 容器运行时启动命令CMD echo "NACOS_ADDR: ${NACOS_ADDR}"; \ echo "JAVA_OPTS: ${JAVA_OPTS}"; \ echo "TZ: ${TZ}"; \ echo "ARTIFACT_ID: ${ARTIFACT_ID}"; \ # 去除了 server 的应用名 REAL_APP_NAME=${ARTIFACT_ID//-server/}; \ echo "REAL_APP_NAME: ${REAL_APP_NAME}"; \ # 获取当前时间 now=`date +%F+%T+%Z`; \ # java 启动命令 java $JAVA_OPTS \ -Dtingyun.app_name=${REAL_APP_NAME}-${TINGYUN_SUFFIX} \ -Dspring.cloud.nacos.discovery.metadata.lemes-env=product \ -Dspring.cloud.nacos.discovery.metadata.startup-time=${now} \ -Dspring.cloud.nacos.discovery.server-addr=${NACOS_ADDR} \ -Dspring.cloud.nacos.discovery.group=${NACOS_GROUP} \ -Dspring.cloud.nacos.config.namespace=${NACOS_NAMESPACE} \ -Dspring.cloud.nacos.discovery.namespace=${NACOS_NAMESPACE} \ -Dspring.cloud.nacos.discovery.ip=${HOST_IP} \ org.springframework.boot.loader.JarLauncher
const devIp = getLocalIP('10.')module.exports = { devServer: { proxy: { '/lemes-api': { target: 'http://10.176.66.58/lemes-api', ws: true, pathRewrite: { '^/lemes-api': '/' }, headers: { 'dev-ip': devIp, 'dev-sc': 'true' } } } },}// 获取本机 IPfunction getLocalIP(prefix) { const excludeNets = ['docker', 'cni', 'flannel', 'vi', 've'] const os = require('os') const osType = os.type() // 系统类型 const netInfo = os.networkInterfaces() // 网络信息 const ipList = [] if (prefix) { for (const netInfoKey in netInfo) { if (excludeNets.filter(item => netInfoKey.startsWith(item)).length === 0) { for (let i = 0; i < netInfo[netInfoKey].length; i++) { const net = netInfo[netInfoKey][i] if (net.family === 'IPv4' && net.address.startsWith(prefix)) { ipList.push(net.address) } } } } } if (ipList.length === 0) { if (osType === 'Windows_NT') { for (const dev in netInfo) { // win7的网络信息中显示为本地连接,win10显示为以太网 if (dev === '本地连接' || dev === '以太网') { for (let j = 0; j < netInfo[dev].length; j++) { if (netInfo[dev][j].family === 'IPv4') { ipList.push(netInfo[dev][j].address) } } } } } else if (osType === 'Linux') { ipList.push(netInfo.eth0[0].address) } else if (osType === 'Darwin') { ipList.push(netInfo.en0[0].address) } } console.log('识别到的网卡信息', JSON.stringify(ipList)) return ipList.length > 0 ? ipList[0] : ''}
不论是 gateway
还是 openfeign
都是通过 spring 的 loadbalancer
进行应用选择的, 那我们通过实现或者继承 ReactorServiceInstanceLoadBalancer
来重写选择的过程.
@Log4j2public class LemesLoadBalancer implements ReactorServiceInstanceLoadBalancer{ @Autowired private NacosDiscoveryProperties nacosDiscoveryProperties; final AtomicInteger position; // loadbalancer 提供的访问当前服务的名称 final String serviceId; // loadbalancer 提供的访问的服务列表 ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; public LemesLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) { this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(1000)); } public LemesLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, int seedPosition) { this.serviceId = serviceId; this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; this.position = new AtomicInteger(seedPosition); } @Override public Mono<Response<ServiceInstance>> choose(Request request) { ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider .getIfAvailable(NoopServiceInstanceListSupplier::new); RequestDataContext context = (RequestDataContext) request.getContext(); RequestData clientRequest = context.getClientRequest(); return supplier.get(request).next() .map(serviceInstances -> processInstanceResponse(clientRequest,supplier, serviceInstances)); } private Response<ServiceInstance> processInstanceResponse(RequestData clientRequest,ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) { Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(clientRequest,serviceInstances); if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) { ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer()); } return serviceInstanceResponse; } private Response<ServiceInstance> getInstanceResponse(RequestData clientRequest, List<ServiceInstance> instances) { if (instances.isEmpty()) { if (log.isWarnEnabled()) { log.warn("No servers available for service: " + serviceId); } return new EmptyResponse(); } int pos = Math.abs(this.position.incrementAndGet()); // 筛选后的服务列表 List<ServiceInstance> filteredInstances; String devSmartConnect = clientRequest.getHeaders().getFirst(CommonConstants.DEV_SMART_CONNECT); if (StrUtil.equals(devSmartConnect, "true")) { String devIp = clientRequest.getHeaders().getFirst(CommonConstants.DEV_IP); // devIp 为空,为异常情况不处理,返回空实例集合 if (StrUtil.isBlank(devIp)) { log.warn("devIp is NULL,No servers available for service: " + serviceId); return new EmptyResponse(); } // 智能连接: 如果本地启动了服务,则优先访问本地服务,如果本地没有启动,则访问测试环境服务 // 优先调用本地自有服务 filteredInstances = instances.stream().filter(item -> StrUtil.equals(devIp, item.getHost())).collect(Collectors.toList()); // 如果本地服务没有开启,则调用生产/测试服务 if (CollUtil.isEmpty(filteredInstances)) { filteredInstances = instances.stream() .filter(item -> StrUtil.equals(CommonConstants.LEMES_ENV_PRODUCT, item.getMetadata().get("lemes-env"))) .collect(Collectors.toList()); // 解决开发环境无法访问 k8s 集群内 ip 的问题 String oneNacosIp = nacosDiscoveryProperties.getServerAddr().split(",")[0].replaceAll(":[\\s\\S]*", ""); filteredInstances.forEach(item -> { NacosServiceInstance instance = (NacosServiceInstance) item; // cloud 以 80 端口启动,认为是 k8s 内的应用 if (instance.getPort() == 80) { instance.setHost(oneNacosIp); instance.setPort(Integer.parseInt(item.getMetadata().get("port"))); } }); } } else { // 不是智能访问,则只访问一个环境 // 当前服务 ip String currentIp = nacosDiscoveryProperties.getIp(); String lemesEnv = nacosDiscoveryProperties.getMetadata().get("lemes-env"); filteredInstances = instances.stream() .filter(item -> StrUtil.equals(lemesEnv, CommonConstants.LEMES_ENV_PRODUCT) // 访问测试环境 ? StrUtil.equals(CommonConstants.LEMES_ENV_PRODUCT, item.getMetadata().get("lemes-env")) // 访问开发环境 : StrUtil.equals(currentIp, item.getHost())) .collect(Collectors.toList()); } if (filteredInstances.isEmpty()) { log.warn("No oneself servers and beta servers available for service: " + serviceId + ", use other instances"); // 找不到自己注册IP对应的服务和测试服务,则用nacos中其它的服务 filteredInstances = instances; } //最终的返回的 serviceInstance ServiceInstance instance = filteredInstances.get(pos % filteredInstances.size()); return new DefaultResponse(instance); }}
]]>我们的 SpringCloud 是部署在 k8s 上的, 当通过 k8s 进行滚动升级时, 会有请求 500 的情况, 不利于用户体验, 严重的可能造成数据错误的问题
k8s 滚动更新策略介绍
假设我们要升级的微服务在环境上为3个副本的集群, 升级应用时, 会先启动1个新版本的副本, 然后下线一个旧版本的副本, 之后再启动1个新版本的副本, 一次类推,直到所有旧副本都替换新副本.
通过链路追踪分析, 报错的原因分别由以下两种情况
为了解决这个问题, 我们将利用 springboot 的 graceful shutdown 功能和 nacos 的主动下线功能来解决这个问题. 具体思路如下:
比如当我们执行订单微服务(3个副本)滚动更新时
副本4
副本1
, 在关闭之前先通知 nacos 订单服务的副本1
下线, 然后由 nacos 通知给其他应用(nacos2.x 是grpc, 所以通知速度比较快), 这样, 订单服务的副本1
就不会再接收到请求, 然后执行 graceful shutdown(springboot 原生支持, 启用方法可以看后面代码), 所有请求处理完成后关闭应用. 这样就完成了 副本1
的关闭副本5
副本2
(参考第2点副本1
的流程)副本6
副本3
为了实现上面背景中提到的思路, 主要从如下几个方面入手
我们通过创建自定义名为 deregister
的 endpoint
来通知 nacos
下线副
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;import com.alibaba.cloud.nacos.registry.NacosRegistration;import com.alibaba.cloud.nacos.registry.NacosServiceRegistry;import lombok.extern.log4j.Log4j2;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.actuate.endpoint.annotation.Endpoint;import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;import org.springframework.stereotype.Component;@Component@Endpoint(id = "deregister")@Log4j2public class LemesNacosServiceDeregisterEndpoint { @Autowired private NacosDiscoveryProperties nacosDiscoveryProperties; @Autowired private NacosRegistration nacosRegistration; @Autowired private NacosServiceRegistry nacosServiceRegistry; /** * 从 nacos 中主动下线,用于 k8s 滚动更新时,提前下线分流流量 * * @param * @return com.lenovo.lemes.framework.core.util.ResultData<java.lang.String> * @author Yujie Yang * @date 4/6/22 2:57 PM */ @ReadOperation public String endpoint() { String serviceName = nacosDiscoveryProperties.getService(); String groupName = nacosDiscoveryProperties.getGroup(); String clusterName = nacosDiscoveryProperties.getClusterName(); String ip = nacosDiscoveryProperties.getIp(); int port = nacosDiscoveryProperties.getPort(); log.info("deregister from nacos, serviceName:{}, groupName:{}, clusterName:{}, ip:{}, port:{}", serviceName, groupName, clusterName, ip, port); // 设置服务下线 nacosServiceRegistry.setStatus(nacosRegistration, "DOWN"); return "success"; }}
由于 springboot 原生支持, 我们只需要在 bootstrap.yaml
中添加如下配置即可
server: # 开启优雅下线 shutdown: gracefulspring: lifecycle: # 优雅下线超时时间 timeout-per-shutdown-phase: 5m# 暴露 shutdown 接口management: endpoint: shutdown: enabled: true endpoints: web: exposure: include: shutdown
有了上面两个 API, 接下来就配置到 k8s 上
---apiVersion: apps/v1kind: Deploymentmetadata: name: lemes-service-common labels: app: lemes-service-commonspec: replicas: 2 selector: matchLabels: app: lemes-service-common# strategy:# type: RollingUpdate# rollingUpdate:## replicas - maxUnavailable < running num < replicas + maxSurge# maxUnavailable: 1# maxSurge: 1 template: metadata: labels: app: lemes-service-common spec:# 容器重启策略 Never Always OnFailure# restartPolicy: Never# 如果关闭时间超过10分钟, 则向容器发送 TERM 信号 terminationGracePeriodSeconds: 600 affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: topologyKey: "kubernetes.io/hostname" labelSelector: matchExpressions: - key: app operator: In values: - lemes-service-common weight: 100# requiredDuringSchedulingIgnoredDuringExecution:# - labelSelector:# matchExpressions:# - key: app# operator: In# values:# - lemes-service-common# topologyKey: "kubernetes.io/hostname" volumes: - name: lemes-host-path hostPath: path: /data/logs type: DirectoryOrCreate - name: sidecar emptyDir: { } containers: - name: lemes-service-common image: 10.176.66.20:5000/lemes-cloud/lemes-service-common-server:v0.1 imagePullPolicy: Always volumeMounts: - name: lemes-host-path mountPath: /data/logs - name: sidecar mountPath: /sidecar ports: - containerPort: 80 resources:# 资源通常情况下的占用 requests: memory: '2048Mi'# 资源占用上限 limits: memory: '4096Mi' livenessProbe: httpGet: path: /actuator/health/liveness port: 80 initialDelaySeconds: 5# 探针可以连续失败的次数 failureThreshold: 10# 探针超时时间 timeoutSeconds: 10# 多久执行一次探针查询 periodSeconds: 10 startupProbe: httpGet: path: /actuator/health/liveness port: 80 failureThreshold: 30 timeoutSeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health/readiness port: 80 initialDelaySeconds: 5 timeoutSeconds: 10 periodSeconds: 10 lifecycle: preStop: exec:# 应用关闭操作:1. 从 nacos 下线,2. 等待30s, 保证 nacos 通知到其他应用 2.触发 springboot 的 graceful shutdown command: - sh - -c - curl http://127.0.0.1/actuator/deregister;sleep 30;curl -X POST http://127.0.0.1/actuator/shutdown;
]]>从第一次接触 vim
已逾期 10 年, 期间大部分都是一些简单操作,
最近一两年开始深度使用 vim
, 目前使用 neovim
版本.
本文将记录一些笔者觉得好用的一些 Plugin
, 本文也将持续更新.
注意: 笔者使用的插件管理器是 vim-plug,
所以以下示例都是基于vim-plug
来写的.
vim-open-url
可以用浏览器打开光标下的 url.
gB
用默认浏览器打开光标下的 urlg<CR>
使用默认搜索引擎搜索光标下的单词gG
使用 Google 搜索光标下的单词gW
使用 Wikipedia 搜索光标下的单词Plug 'dhruvasagar/vim-open-url'
通过指令的方式, 在 drawer
的左侧边缘, 添加一个触发拖拽的长条形区域, 监听鼠标左键按下时启动 document.onmousemove
的监听, 监听鼠标距离浏览器右边的距离, 设置为 drawer
的宽度, 并添加约束: 不能小于浏览器宽度的 20%, 不能大于浏览器宽度的 80%.
创建文件 src/directive/elment-ui/drawer-drag-width.js
, 内容如下
import Vue from 'vue'/** * el-drawer 拖拽指令 */Vue.directive('el-drawer-drag-width', { bind(el, binding, vnode, oldVnode) { const drawerEle = el.querySelector('.el-drawer') console.log(drawerEle) // 创建触发拖拽的元素 const dragItem = document.createElement('div') // 将元素放置到抽屉的左边边缘 dragItem.style.cssText = 'height: 100%;width: 5px;cursor: w-resize;position: absolute;left: 0;' drawerEle.append(dragItem) dragItem.onmousedown = (downEvent) => { // 拖拽时禁用文本选中 document.body.style.userSelect = 'none' document.onmousemove = function(moveEvent) { // 获取鼠标距离浏览器右边缘的距离 let realWidth = document.body.clientWidth - moveEvent.pageX const width30 = document.body.clientWidth * 0.2 const width80 = document.body.clientWidth * 0.8 // 宽度不能大于浏览器宽度 80%,不能小于宽度的 20% realWidth = realWidth > width80 ? width80 : realWidth < width30 ? width30 : realWidth drawerEle.style.width = realWidth + 'px' } document.onmouseup = function(e) { // 拖拽时结束时,取消禁用文本选中 document.body.style.userSelect = 'initial' document.onmousemove = null document.onmouseup = null } } }})
然后在 main.js
中将其导入
import './directive/element-ui/drawer-drag-width'
在 el-drawer
上添加指令 v-el-drawer-drag-width
即可, 如下
<el-drawer v-el-drawer-drag-width :visible.sync="helpDrawer.show" direction="rtl" class="my-drawer"> <template #title> <div class="draw-title">{{ helpDrawer.title }}</div> </template> <Editor v-model="helpDrawer.html" v-loading="helpDrawer.loading" class="my-wang-editor" style="overflow-y: auto;" :default-config="helpDrawer.editorConfig" :mode="helpDrawer.mode" @onCreated="onCreatedHelp" /></el-drawer>
]]>前一段时间一直在研究升级公司项目的架构,在不断学习和试错后,最终确定了一套基于 k8s 的高可用架构体系,未来几期会将这套架构体系的架设过程和注意事项以系列文章的形式分享出来,敬请期待!
由于集群和分布式规模的扩大,对微服务链路的监控和日志收集,越来越有必要性,所以在筛选了了一些方案后,发现 SkyWalking 完美符合我们的预期,对链路追踪和日志收集都有不错的实现。
SkyWalking 是一款 APM(应用程序监控)系统,转为微服务、云原生、基于容器的架构而设计。主要包含了一下核心功能
开源地址:apache/skywalking
在使用 SkyWalking 进行链路追踪和日志收集之前,需要先搭建起一套 SkyWalking 的服务,然后才能通过 agent 将 SpringCloud 的运行状态和日志发送给 SkyWalking 进行解析和展示。
SkyWalking 的搭建方式有很多中,我这里介绍两种 docker-compose(非高可用,快速启动,方便测试、学习) 和 k8s(高可用、生产级别)
docker 和 docker-compose 的安装不是本文的重点,所以有需要可以自行查询。
以下操作会启动三个容器
elasticsearch
作为 skywalking 的存储,保存链路和日志数据等oap
数据接收和分析 Observability Analysis Platformui
web端的数据展示# 创建配置文件保存的目录mkdir -p /data/docker/admin/skywalking# 切换到刚创建的目录cd /data/docker/admin/skywalking# 将下面的 docker-compose.yml 文件保存到这个目录vi docker-compose.yml# 拉去镜像并启动docker-compose up -d# 查看日志docker-compose logs -f
docker-compose.yml
version: '3.8'services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.14.1 container_name: elasticsearch restart: always ports: - 9200:9200 healthcheck: test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 40s environment: - discovery.type=single-node - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - TZ=Asia/Shanghai ulimits: memlock: soft: -1 hard: -1 oap: image: apache/skywalking-oap-server:8.7.0-es7 container_name: oap depends_on: - elasticsearch links: - elasticsearch restart: always ports: - 11800:11800 - 12800:12800 healthcheck: test: ["CMD-SHELL", "/skywalking/bin/swctl"] interval: 30s timeout: 10s retries: 3 start_period: 40s environment: TZ: Asia/Shanghai SW_STORAGE: elasticsearch7 SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200 ui: image: apache/skywalking-ui:8.7.0 container_name: ui depends_on: - oap links: - oap restart: always ports: - 8088:8080 environment: TZ: Asia/Shanghai SW_OAP_ADDRESS: http://oap:12800
启动之后浏览器访问 服务ip:8080
即可
等待更新。。
点击链接进行下载,skywalking-apm-8.7
其他版本可以看 apache 归档站,找到对应版本的
.tar.gz
后缀的包,进行下载
通过命令或者软件进行解压 tar -zxvf apache-skywalking-apm-8.7.0.tar.gz
springcloud/springboot 一般是通过 java -jar xxx.jar
进行启动。我们只需要在其中加上 -javaagent
参数即可,如下
其中 自定义服务名 可以改为应用名 如 lemes-auth
,服务ip 为第一步搭建的 SkyWalking 服务的ip,端口11800 为启动的 oap 这个容器的端口
java -javaagent:上一步解压目录/agent/skywalking-agent.jar=agent.service_name=自定义服务名,collector.backend_service=服务ip:11800 -jar xx.jar
执行命令启动后,访问以下接口,就可以在第一步 服务ip:8080
中看到访问的链接和调用链路。
本文主要以 log4j2 来介绍,其他的大同小异,可以网上找教程。SpringCloud 集成 log4j2 不是本文重点,所以请自行 Google。
要开启日志收集,必须要添加依赖,如下
<dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-log4j-2.x</artifactId> <version>8.7.0</version></dependency>
需要修改 log4j2.xml 主要添加下面两个关键点
%traceId
来打印 traceid完整内容如下
<?xml version="1.0" encoding="UTF-8"?><!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --><!-- Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时, 你会看到log4j2内部各种详细输出。可以设置成OFF(关闭) 或 Error(只输出错误信息)。--><!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数--><configuration status="WARN" monitorInterval="30"> <Properties> <Property name="log.path">logs/lemes-auth</Property> <Property name="logging.lemes.pattern"> %d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%traceId] [%logger{50}.%M:%L] - %msg%n </Property> </Properties> <Appenders> <!-- 输出控制台日志的配置 --> <Console name="Console" target="SYSTEM_OUT"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/> <!-- 输出日志的格式 --> <PatternLayout pattern="${logging.lemes.pattern}"/> </Console> <RollingRandomAccessFile name="debugRollingFile" fileName="${log.path}/debug.log" filePattern="${log.path}/debug/$${date:yyyy-MM}/debug.%d{yyyy-MM-dd}-%i.log.gz"> <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout charset="UTF-8" pattern="${logging.lemes.pattern}"/> <Policies> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> <DefaultRolloverStrategy max="30"/> </RollingRandomAccessFile> <GRPCLogClientAppender name="grpc-log"> <PatternLayout pattern="${logging.lemes.pattern}"/> </GRPCLogClientAppender> </Appenders> <Loggers> <!-- ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF --> <Logger name="com.lenovo.lemes" level="debug"/> <Logger name="org.apache.kafka" level="warn"/> <Root level="info"> <AppenderRef ref="Console"/> <AppenderRef ref="debugRollingFile"/> <AppenderRef ref="grpc-log"/> </Root> </Loggers></configuration>
在上一步的 agent 中添加上报日志的参数 plugin.toolkit.log.grpc.reporter.server_host=服务ip,plugin.toolkit.log.grpc.reporter.server_port=11800
完整如下
java -javaagent:上一步解压目录/agent/skywalking-agent.jar=agent.service_name=自定义服务名,collector.backend_service=服务ip:11800,plugin.toolkit.log.grpc.reporter.server_host=服务ip,plugin.toolkit.log.grpc.reporter.server_port=11800 -jar xx.jar
这样启动日志中就会打印 traceid , N/A
代表的是非请求的日志,有 traceid 的为 api 请求日志
在 skywalking 中就能看到我们上报的日志
重点:SkyWalking 可以在链路追踪中查看当前请求的所有日志(不同实例/模块)
经过上面的步骤之后,链路已经搭建完成,查看发现了一个问题,gateway 模块的 traceId
和 业务模块的 traceId
不统一。
这是由于 SkyWalking 对于 spring-cloud-gateway
的支持不是默认的,所以需要将 agent/optional-plugins/apm-spring-cloud-gateway-2.1.x-plugin-8.7.0.jar
复制到 agent/plugins
下,然后重启即可。
SkyWalking 上面这两个功能就已经非常强大,能够有效帮助我们优化我们的程序,监控系统的问题,并及时报警。日志收集也解决的在大规模分布式集群下日志查询难的问题。
SkyWalking 还支持 VM、浏览器、k8s等监控,后续如果有实践,将会逐步更新。
]]>鉴于许多人问过如何添加自定义图标,这里就详细说明一下,以备后人乘凉。
这篇文章主要讲解是从 iconfont 添加图标。
访问 iconfont,点击如下图位置登录,可以使用 Github
账号登录。
登录成功后,搜索合适的图标,然后点击添加到购物车,如下图所示。
添加了多个后,可以点击右上角的“购物车”,添加到项目,点击加号创建项目,如下图所示。
添加完成后回到项目页面,找到自己刚刚创建的项目。
如果没有到项目页面,可以点击上面菜单进入:资源管理 -> 我的项目
点击下载到本地,解压并复制其中的 iconfont.js
到项目 3-hexo/source/js/
下,并改名 custom-iconfont.js
。
在文件 3-hexo/layout/_partial/meta.ejs
最后追加下面一行。
<script src="<%=theme.blog_path?theme.blog_path.lastIndexOf("/") === theme.blog_path.length-1?theme.blog_path.slice(0, theme.blog_path.length-1):theme.blog_path:'' %>/js/custom-iconfont.js?v=<%=theme.version%>" ></script>
修改 3-hexo/_config.yml
如下图所示
完成!
图标名如上面的
gitee
可以在 网站上修改,如下图所示
link.theme=white
点击生成代码,如下图所示。
复制生成的代码,修改 font-family
的值为 custom-iconfont
,添加到 3-hexo/source/css/_partial/font.styl
最后,并写入图标信息,content
可以移到图标上进行复制,注意前面斜杠转译和去掉后面的分号。
@font-face { font-family: 'custom-iconfont'; /* project id 2298064 */ src: url('//at.alicdn.com/t/font_2298064_34vkk4c9945.eot'); src: url('//at.alicdn.com/t/font_2298064_34vkk4c9945.eot?#iefix') format('embedded-opentype'), url('//at.alicdn.com/t/font_2298064_34vkk4c9945.woff2') format('woff2'), url('//at.alicdn.com/t/font_2298064_34vkk4c9945.woff') format('woff'), url('//at.alicdn.com/t/font_2298064_34vkk4c9945.ttf') format('truetype'), url('//at.alicdn.com/t/font_2298064_34vkk4c9945.svg#iconfont') format('svg');}.icon-gitee:before { content: "\e602";}.icon-youtubeautored:before { content: "\e649";}
结束!
]]>Promise
是 ES6
提供的原生对象,用来处理异步操作
它有三种状态
pending
: 初始状态,不是成功或失败状态。fulfilled
: 意味着操作成功完成。rejected
: 意味着操作失败。通过 new Promise
来实例化,支持链式调用
new Promise((resolve, reject)=>{ // 逻辑}).then(()=>{ //当上面"逻辑"中调用 resolve() 时触发此方法}).catch(()=>{ //当上面"逻辑"中调用 reject() 时触发此方法})
Promise
一旦创建就立即执行,并且无法中途取消,执行逻辑和顺序可以从下面的示例中获得
如下,可修改 if
条件来改变异步结果,下面打印开始的数字是执行顺序
console.log('1.开始创建并执行 Promise')new Promise(function(resolve, reject) { console.log('2.由于创建会立即执行,所以会立即执行到本行') setTimeout(()=>{ // 模拟异步请求 console.log('4. 1s之期已到,开始执行异步操作') if (true) { // 一般我们符合预期的结果时调用 resolve(),会在 .then 中继续执行 resolve('成功') } else { // 不符合预期时调用 reject(),会在 .catch 中继续执行 reject('不符合预期') } }, 1000)}).then((res)=>{ console.log('5.调用了then,接收数据:' + res)}).catch((error)=>{ console.log('5.调用了catch,错误信息:' + error)})console.log('3.本行为同步操作,所以先于 Promise 内的异步操作(setTimeout)')
执行结果如下
"1.开始创建并执行 Promise""2.由于创建会立即执行,所以会立即执行到本行""3.本行为同步操作,所以先于 Promise 内的异步操作(setTimeout)""4. 1s之期已到,开始执行异步操作""5.调用了then,接收数据:成功"
这是比较常用的方法,如下用 setTimeout
模拟异步请求,封装通用请求函数
// 这是一个异步方法function ajax(url){ return new Promise(resolve=>{ console.log('异步方法开始执行') setTimeout(()=>{ console.log('异步方法执行完成') resolve(url+'的结果集') }, 1000) })}// 调用请求函数,并接受处理返回结果ajax('/user/list').then((res)=>{ console.log(res)})
执行结果
"异步方法开始执行""异步方法执行完成""/user/list的结果集"
function ajax(url, success, fail) { if (typeof success === 'function') { setTimeout(() => { if (true) { success({user: '羊'}) } else if (typeof fail === 'function') { console.log(typeof fail) fail('用户不存在') } }, 1000) } else { return new Promise((resolve, reject) => { this.ajax(url, resolve, reject) }) }}// callback 调用方式ajax('/user/get', (res)=>{ console.log('Callback请求成功!返回结果:', res)}, (error)=>{ console.log('Callback请求失败!错误信息:', error)})// Promise 调用方式ajax('/user/get').then((res)=>{ console.log('Pormise请求成功!返回结果:', res)}).catch((error)=>{ console.log('Promise请求失败!返回结果:', error)})
执行结果
Callback请求成功!返回结果: {user: "羊"}Pormise请求成功!返回结果: {user: "羊"}
.then
支持返回 Promise
对象进行链式调用
ajax('/user/info').then((res)=>{ // 用户信息查询成功后,可以根据返回结果查询后续信息 console.log('用户信息:', res) return ajax('/user/score')}).then((res)=>{ console.log('用户成绩:', res) return ajax('/user/friends')}).then((res)=>{ console.log('用户朋友:', res)})
Promise.all
方法用于将多个 Promise
实例,包装成一个新的 Promise
实例。
在线调试此示例 - jsbin
// 生成一个Promise对象的数组var promises = [2, 3, 5, 7, 11, 13].map(function(id){ return new Promise((resolve, reject)=>{ if (id % 3 === 0) { resolve(id) } else { reject(id) } });});Promise.all(promises).then(function(post) { console.log('全部通过')}).catch(function(reason){ console.log('未全部通过,有问题id:'+reason)});
执行结果
未全部通过,有问题id:2
Docker 诞生于 2013 年初,由 dotCloud 公司(后改名为 Docker Inc)基于 Go 语言实现并开源的项目。此项目后来加入 Linux基金会,遵从了 Apache 2.0 协议
Docker 项目的目标是实现轻量级的操作系统虚拟化解决方案。Docker 是在 Linux 容器技术(LXC)的基础上进行了封装,让用户可以快速并可靠的将应用程序从一台运行到另一台上。
使用容器部署应用被称为容器化,容器化技术的几大优势:
容器在Linux上本地运行,并与其他容器共享主机的内核。它运行一个离散进程,不占用任何其他可执行文件更多的内存,从而使其轻巧。
Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
可以通过 docker pull
命令从仓库获取所需要的镜像
docker pull [选项] [Docker Registry 地址]<镜像名>:<标签>
选项:
标签: 下载指定标签的镜像,默认 latest
示例
# 从 Docker Hub 下载最新的 debian 镜像docker pull debian# 从 Docker Hub 下载 jessie 版 debian 镜像docker pull debian:jessie# 下载指定摘要(sha256)的镜像docker pull ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2
# 列出已下载的镜像 image_name: 指定列出某个镜像docker images [选项] [image_name]
选项
参数 | 描述 |
---|---|
–all, -a | 展示所有镜像(包括 intermediate 镜像) |
–digests | 展示摘要 |
–filter, -f | 添加过滤条件 |
–format | 使用 Go 模版更好的展示 |
–no-trunc | 不删减输出 |
–quiet, -q | 静默输出,仅仅展示 IDs |
示例
# 展示本地所有下载的镜像docker images# 在本地查找镜像名是 "java" 标签是 "8" 的 奖项docker images: java:8# 查找悬挂镜像docker images --filter "dangling=true"# 过滤 lable 为 "com.example.version" 的值为 0.1 的镜像docker images --filter "label=com.example.version=0.1"
为了方便分享和快速部署,我们可以使用 docker build
来创建一个新的镜像,首先创建一个文件 Dockerfile,如下
# This is a commentFROM ubuntu:14.04MAINTAINER Chris <jaytp@qq.com>RUN apt-get -qq updateRUN apt-get -qqy install ruby ruby-devRUN gem install sinatra
然后在此 Dockerfile 所在目录执行 docker build -t yelog/ubuntu:v1 .
来生成镜像,所属组织/镜像名:标签
用户可以通过 docker push
命令,把自己创建的镜像上传到仓库中来共享。例如,用户在 Docker Hub 上完成注册后,可以推送自己的镜像到仓库中。
docker push yelog/ubuntu
docker 支持将镜像导出为文件,然后可以再从文件导入到本地镜像仓库
# 导出docker load --input yelog_ubuntu_v1.tar# 载入docker load < yelog_ubuntu_v1.tar
# -f 强制删除docker rmi [-f] yelog/ubuntu:v1# 删除悬挂镜像docker rmi $(docker images -f "dangling=true" -q)# 删除所有未被容器使用的镜像docker image prune -a
容器和镜像,就像面向对象中的 类 和 示例 一样,镜像是静态的定义,容器是镜像运行的实体,容器可以被创建、启动、停止、删除和暂停等
容器的实质是进城,耽于直接的宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的 root 文件系统、网络配置和进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。
我们可以通过命令 docker run
命令创建容器
如下,启动一个容器,执行命令输出 “Hello word”,之后终止容器
docker run ubuntu:14.04 /bin/echo 'Hello world'
下面的命令则是启动一个 bash 终端,允许用户进行交互
docker run -t -i ubuntu:14.04 /bin/bash
-t
让 Dcoker 分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上
-i
责让容器的标准输入保持打开
更多参数可选
-a stdin | 指定标准输入输出内容类型 |
---|---|
-d | 后台运行容器,并返回容器ID |
-i | 以交互模式运行容器,通常与 -t 同时使用 |
-P | 随机端口映射,容器端口内部随即映射到宿主机的端口上 |
-p | 指定端口映射, -p 宿主机端口:容器端口 |
-t | 为容器重新分配一个伪输入终,通常与 -i 同时使用 |
–name=”gate” | 为容器指定一个名称 |
–dns 8.8.8.8 | 指定容器的 DNS 服务器,默认与宿主机一致 |
–dns-search example.com | 指定容器 DNS 搜索域名,默认与宿主机一致 |
-h “gate” | 指定容器的 hostname |
-e username=’gate’ | 设置环境变量 |
–env-file=[] | 从指定文件读入环境变量 |
–cpuset=”0-2” or –cpuset=”0,1,2” | 绑定容器到指定 CPU 运行 |
-m | 设置容器使用内存最大值 |
–net=”bridge” | 指定容器的网络连接类型支持 bridge/host/none/container |
–link=[] | 添加链接到另一个容器 |
–expose=[] | 开放一个端口或一组端口 |
–volume,-v | 绑定一个卷 |
当利用 docker run
来创建容器时,Dcoker 在后台运行的标准操作包括:
# 创建一个名为 test 的容器,容器任务是:打印一行 Hello worddocker run --name='test' ubuntu:14.04 /bin/echo 'Hello world'# 查看所有可用容器 [-a]包括终止在内的所有容器docker ps -a# 启动指定 name 的容器docker start test# 重启指定 name 的容器docker restart test# 查看日志运行日志(每次启动的日志均被查询出来)$ docker logs testHello worldHello world
前面创建的容器都是执行任务(打印Hello world)后,容器就终止了。更多的时候,我们需要让 Docker 容器在后台以守护态(Daemonized)形式运行。此时,可以通过添加 -d
参数来实现
注意:docker是否会长久运行,和 docker run 指定的命令有关
# 创建 docker 后台守护进程的容器docker run --name='test2' -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"# 查看容器$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES237e555d4457 ubuntu:14.04 "/bin/sh -c 'while t…" 52 seconds ago Up 51 seconds test2# 获取容器的输出信息$ docker logs test2hello worldhello worldhello world
上一步我们已经实现了容器守护态长久运行,某些时候需要进入容器进行操作,可以使用 attach
、exec
进入容器。
# 不安全的,ctrl+d 退出时容器也会终止docker attach [容器Name]# 以交互式命令行进入,安全的,推荐使用docker exec -it [容器Name] /bin/bash
命令优化
docker exec
命令时,好用,但是命令过长,我们可以通过自定义命令来简化使用/user/bin/ctn
命令文件,内容如下docker exec -it $1 /bin/bash
/usr/bin
(一般是有配置在环境变量里面的,不过最好再确认一下)$PATHbash: /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games: No such file or directory
ctn
来进入容器注意:如果是使用非 root 账号创建的命令,而 docker 命令是 root 权限,可能存在权限问题,可以通过设置
chmod 777 /usr/bin/ctn
设置权限,使用sudo ctn [容器Name]
即可进入容器
$ ctn [容器Name]
complete
命令来补全 容器Name,在 ~/.bashrc
(作用于当前用户,如果想要所要用户上校,可以修改 /etc/bashrc
)文件中添加一行,内容如下。保存后执行 source ~/.bashrc
使之生效,之后我们输入 ctn
后,按 tab
就会提示或自动补全容器名了了# ctn auto completecomplete -W "$(docker ps --format"{{.Names}}")" ctn
注意: 由于提示的 容器Name 是
~/.bashrc
生效时的列表,所有如果之后 docker 容器列表有变动,需要重新执行source ~/.bashrc
使之更新提示列表
通过 docker stop [容器Name]
来终止一个运行中的容器
# 终止容器名为 test2 的容器docker stop test2# 查看正在运行中的容器docker ps# 查看所有容器(包括终止的)docker ps -a
我们修改一个容器后,可以经当前容器状态打包成镜像,方便下次直接通过镜像仓库生成当前状态的容器。
# 创建容器docker run -t -i training/sinatra /bin/bash# 添加两个应用gem install json# 将修改后的容器打包成新的镜像docker commit -m "Added json gem" -a "Docker Newbee" 0b2616b0e5a8 ouruser/sinatra:v2
容器 ->导出> 容器快照文件 ->导入> 本地镜像仓库 ->新建> 容器
$ docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES2a8bffa405c8 ubuntu:14.04 "/bin/sh -c 'while t…" About an hour ago Up 3 seconds test2# 导出$ docker export 2a8bffa405c8 > ubuntu.tar# 导入为镜像$ docker ubuntu.tar | docker import - test/ubuntu:v1.0# 从指定 URL 或者某个目录导入$ docker import http://example.com/exampleimage.tgz example/imagerepo
注意:用户既可以通过
docker load
来导入镜像存储文件到本地镜像仓库,也可以使用docker import
来导入一个容器快找到本地镜像仓库,两者的区别在于容器快照将丢失所有的历史记录和元数据信息,仅保存容器当时的状态,而镜像存储文件将保存完成的记录,体积要更大。所有容器快照文件导入时需要重新指定标签等元数据信息。
可以使用 docker rm [容器Name]
来删除一个终止状态的容器,如果容器还未终止,可以先使用 docker stop [容器Name]
来终止容器,再进行删除操作
docker rm test2# 删除容器 -f: 强制删除,无视是否运行$ docker [-f] rm myubuntu# 删除所有已关闭的容器$ docker rm $(docker ps -a -q)
docker stats $(docker ps --format={{.Names}})
数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多特性:
数据卷类似于 Linux 下对目录或文件进行 mount
在用 docker run
命令的时候,使用 -v
标记来创建一个数据卷并挂在在容器里,可同时挂在多个。
# 创建一个 web 容器,并加载一个数据卷到容器的 /webapp 目录docker run -d -P --name web -v /webapp training/webapp python app.py# 挂载一个宿主机目录 /data/webapp 到容器中的 /opt/webappdocker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py# 默认是读写权限,也可以指定为只读docker run -d -P --name web -v /src/webapp:/opt/webapp:ro# 挂载单个文件docker run --rm -it -v ~/.bash_history:/.bash_history ubuntu /bin/bash
如果需要多个容器共享数据,最好创建数据卷容器,就是一个正常的容器,撰文用来提供数据卷供其他容器挂载的
# 创建一个数据卷容器 dbdatadocker run -d -v /dbdata --name dbdata training/postgres echo Data-only container for postgres# 其他容器挂载 dbdata 容器的数据卷docker run -d --volumes-from dbdata --name db1 training/postgresdocker run -d --volumes-from dbdata --name db2 training/postgres
在容器内运行一些服务,需要外部可以访问到这些服务,可以通过 -P
或 -p
参数来指定端口映射。
当使用 -P
标记时,Docker 会随即映射一个 49000~49900
的端口到内部容器开放的网络端口。
使用 docker ps
可以查看端口映射情况
$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES7f43807dc042 training/webapp "python app.py" 3 seconds ago Up 2 seconds 0.0.0.0:32770->5000/tcp amazing_liskov
-p 指定端口映射,支持格式 ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort
# 不限制ip访问docker run -d -p 5000:5000 training/webapp python app.py# 只允许宿主机回环地址访问docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py# 宿主机自动分配绑定端口docker run -d -p 127.0.0.1::5000 training/webapp python app.py# 指定 udp 端口docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py# 指定多个端口映射docker run -d -p 5000:5000 -p 3000:80 training/webapp python app.py# 查看映射端口配置$ docker port amazing_liskov5000/tcp -> 0.0.0.0:32770
容器除了跟宿主机端口映射外,还有一种容器间交互的方式,可以在源/目标容器之间建立一个隧道,目标容器可以看到源容器指定的信息。
可以通过 --link name:alias
来连接容器,下面就是 “web容器连接db容器” 的例子
# 创建 容器dbdocker run -d --name db training/postgres# 创建 容器web 并连接到 容器dbdocker run -d -P --name web --link db:db training/webapp python app.py# 进入 容器web,测试连通性$ ctn web$ ping dbPING db (172.17.0.3) 56(84) bytes of data.64 bytes from db (172.17.0.3): icmp_seq=1 ttl=64 time=0.254 ms64 bytes from db (172.17.0.3): icmp_seq=2 ttl=64 time=0.190 ms64 bytes from db (172.17.0.3): icmp_seq=3 ttl=64 time=0.389 ms
容器想要访问外部网络,需要宿主机的转发支持。在 Linux 系统中,通过以下命令检查是否打开
$ sysctl net.ipv4.ip_forwardnet.ipv4.ip_forward = 1
如果是 0,说明没有开启转发,则需要手动打开。
$ sysctl -w net.ipv4.ip_forward=1
Docker 服务默认会创建一个 docker0
网桥,他在内核层连通了其他物理或虚拟网卡,这就将容器和主机都放在同一个物理网络。
Docker 默认制定了 docker0
接口的IP地址和子网掩码,让主机和容器间可以通过网桥相互通信,他还给了 MTU(接口允许接收的最大单元),通常是 1500 Bytes,或宿主机网络路由上支持的默认值。这些都可以在服务启动的时候进行配置。
--bip=CIDR
ip地址加子网掩码格式,如 192.168.1.5/24--mtu=BYTES
覆盖默认的 Docker MTU 配置可以通过 brctl show
来查看网桥和端口连接信息
Docker 1.2.0 开始支持在运行中的容器里编辑 /etc/hosts
、/etc/hostsname
和 /etc/resolve.conf
文件,修改都是临时的,重新容器将会丢失修改,通过 docker commit
也不会被提交。
Dockerfile 是由一行行命令组成的命令集合,分为四个部分:
如下:
# 最前面一般放这个 Dockerfile 的介绍、版本、作者及使用说明等# This dockerfile uses the ubuntu image# VERSION 2 - EDITION 1# Author: docker_user# Command format: Instruction [arguments / command] ..# 使用的基础镜像,必须放在非注释第一行FROM ubuntu# 维护着信息信息: 名字 联系方式MAINTAINER docker_user docker_user@email.com# 构建镜像的命令:对镜像做的调整都在这里RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.listRUN apt-get update && apt-get install -y nginxRUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf# 创建/运行 容器时的操作指令 # 可以理解为 docker run 后跟的运行指令CMD /usr/sbin/nginx
指令一般格式为 INSTRUCTION args
,包括 FORM
、 MAINTAINER
、RUN
等
FORM | 第一条指令必须是 FORM 指令,并且如果在同一个Dockerfile 中创建多个镜像,可以使用多个 FROM 指令(每个镜像一次) | FORM ubuntuFORM ubuntu:14.04 |
---|---|---|
MAINTAINER | 维护者信息 | MAINTAINER Chris xx@gmail.com |
RUN | 每条 RUN 指令在当前镜像基础上执行命令,并提交为新的镜像。当命令过长时可以使用 \ 来换行 | 在 shell 终端中运行命令RUN apt-get update && apt-get install -y nginx 在 exec 中执行:RUN ["/bin/bash", "-c", "echo hello"] |
CMD | 指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会被执行。 | CMD ["executable","param1","param2"] 使用 exec 执行,推荐方式;CMD command param1 param2 在 /bin/sh 中执行,提供给需要交互的应用;CMD ["param1","param2"] 提供给 ENTRYPOINT 的默认参数; |
EXPOSE | 告诉服务端容器暴露的端口号, | EXPOSE |
ENV | 指定环境变量 | ENV PG_MAJOR 9.3ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH |
ADD | ADD 该命令将复制指定的 到容器中的 。其中 可以是 Dockerfile 所在目录的一个相对路径,也可以是一个URL ;还可以是一个 tar文件(自动解压为目录) | |
COPY | 格式为 COPY 复制本地主机的 (为 Dockerfile 所在目录的相对路径)到容器中的 。当使用本地目录为源目录时,推荐使用 COPY | |
ENTRYPOINT | 配置容器启动执行的命令,并且不可被 docker run 提供的参数覆盖每个Docekrfile 中只能有一个 ENTRYPOINT ,当指定多个时,只有最后一个起效 | 两种格式ENTRYPOINT ["executable", "param1", "param2"]``ENTRYPOINT command param1 param2 (shell中执行) |
VOLUME | 创建一个可以从本地主机或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等。 | VOLUME [“/data”] |
USER | 指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户 | USER daemon |
WORKDIR | 为后续的 RUN 、CMD 、ENTRYPOINT 指令配置工作目录。可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。 | 格式为 WORKDIR /path/to/workdir 。 WORKDIR /aWORKDIR bWORKDIR cRUN pwd最后的路径为 /a/b/c |
ONBUILD | 配置当所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令。 | 格式为 ONBUILD [INSTRUCTION] 。 |
编写完成 Dockerfile 之后,可以通过 docker build
命令来创建镜像
docker build [选项] 路径
该命令江都区指定路径下(包括子目录)的Dockerfile,并将该路径下所有内容发送给 Docker 服务端,有服务端来创建镜像。可以通过 .dockerignore
文件来让 Docker 忽略路径下的目录与文件
# 使用 -t 指定镜像的标签信息docker build -t myrepo/myimage .
Docker Compose 是 Docker 官方编排项目之一,负责快速在集群中部署分布式应用。维护地址:https://github.com/docker/compose,由 Python 编写,实际调用 Docker提供的API实现。
Dockerfile 可以让用户管理一个单独的应用容器,而 Compose 则允许用户在一个模版(YAML格式)中定义一组相关联的应用容器(被称为一个project/项目),例如一个 web容器再加上数据库、redis等。
# 使用 pip 进行安装pip install -U docker-compose# 查看用法docker-ompose -h# 添加 bash 补全命令curl -L https://raw.githubusercontent.com/docker/compose/1.2.0/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
术语
示例:创建一个 Haproxy 挂载三个 Web 容器
创建一个 compose-haproxy-web
目录,作为项目工作目录,并在其中分别创建两个子目录: haproxy
和 web
。
compose-haproxy-webcompose-haproxy-webgit clone https://github.com/yelog/compose-haproxy-web.git
目录长这样:
compose-haproxy-web├── docker-compose.yml├── haproxy│ └── haproxy.cfg└── web ├── Dockerfile ├── index.html └── index.py
在该目录执行 docker-compose up
命令,会整合输出所有容器的输出
$ docker-compose upStarting compose-haproxy-web_webb_1 ... doneStarting compose-haproxy-web_webc_1 ... doneStarting compose-haproxy-web_weba_1 ... doneRecreating compose-haproxy-web_haproxy_1 ... doneAttaching to compose-haproxy-web_webb_1, compose-haproxy-web_weba_1, compose-haproxy-web_webc_1, compose-haproxy-web_haproxy_1haproxy_1 | [NOTICE] 244/131022 (1) : haproxy version is 2.2.2haproxy_1 | [NOTICE] 244/131022 (1) : path to executable is /usr/local/sbin/haproxyhaproxy_1 | [ALERT] 244/131022 (1) : parsing [/usr/local/etc/haproxy/haproxy.cfg:14] : 'listen' cannot handle unexpected argument ':70'.haproxy_1 | [ALERT] 244/131022 (1) : parsing [/usr/local/etc/haproxy/haproxy.cfg:14] : please use the 'bind' keyword for listening addresses.haproxy_1 | [ALERT] 244/131022 (1) : Error(s) found in configuration file : /usr/local/etc/haproxy/haproxy.cfghaproxy_1 | [ALERT] 244/131022 (1) : Fatal errors found in configuration.compose-haproxy-web_haproxy_1 exited with code 1
此时访问本地的 80 端口,会经过 haproxy 自动转发到后端的某个 web 容器上,刷新页面,可以观察到访问的容器地址的变化。
大部分命令都可以运行在一个或多个服务上。如果没有特别的说明,命令则应用在项目所有的服务上。
执行 docker-compose [COMMAND] --help
查看具体某个命令的使用说明
使用格式
docker-compose [options] [COMMAND] [ARGS...]
build | 构建/重建服务服务一旦构建后,将会带上一个标记名,例如 web_db可以随时在项目目录运行 docker-compose build 来重新构建服务 |
---|---|
help | 获得一个命令的信息 |
kill | 通过发送 SIGKILL 信号来强制停止服务容器,支持通过参数来指定发送信号,例如docker-compose kill -s SIGINT |
logs | 查看服务的输出 |
port | 打印绑定的公共端口 |
ps | 列出所有容器 |
pull | 拉去服务镜像 |
rm | 删除停止的服务容器 |
run | 在一个服务上执行一个命令docker-compose run ubuntu ping docker.com |
scale | 设置同一个服务运行的容器个数通过 service=num 的参数来设置数量docker-compose scale web=2 worker=3 |
start | 启动一个已经存在的服务容器 |
stop | 停止一个已经运行的容器,但不删除。可以通过 docker-compose start 再次启动 |
up | 构建、创建、启动、链接一个服务相关的容器链接服务都将被启动,除非他们已经运行docker-compose up -d 将后台运行并启动docker-compose up 已存在容器将会重新创建docker-compose up --no-recreate 将不会重新创建容器 |
环境变量可以用来配置 Compose 的行为
以 Docker_
开头的变量用来配置 Docker 命令行客户端使用的一样
COMPOSE_PROJECT_NAME | 设置通过 Compose 启动的每一个容器前添加的项目名称,默认是当前工作目录的名字。 |
---|---|
COMPOSE_FILE | 设置要使用的 docker-compose.yml 的路径。默认路径是当前工作目录。 |
DOCKER_HOST | 设置 Docker daemon 的地址。默认使用 unix:///var/run/docker.sock ,与 Docker 客户端采用的默认值一致。 |
DOCKER_TLS_VERIFY | 如果设置不为空,则与 Docker daemon 交互通过 TLS 进行。 |
DOCKER_CERT_PATH | 配置 TLS 通信所需要的验证(ca.pem 、cert.pem 和 key.pem )文件的路径,默认是 ~/.docker 。 |
默认模版文件是 docker-compose.yml
,启动定义了每个服务都必须经过 image
指令指定镜像或 build
指令(需要 Dockerfile) 来自动构建。
其他大部分指令跟 docker run
类似
如果使用 build
指令,在 Dockerfile 中设置的选项(如 CMD
、EXPOSE
等)将会被自动获取,无需在 docker-compose.yml
中再次设置。
**image**
指定镜像名称或镜像ID,如果本地仓库不存在,将尝试从远程仓库拉去此镜像
image: ubuntuimage: orchardup/postgresqlimage: a4bc65fd**build**
指定 Dockerfile
所在文件的路径。Compose
将利用它自动构建这个镜像,然后使用这个镜像。
build: /path/to/build/dir**command**
覆盖容器启动默认执行命令
command: bundle exec thin -p 3000**links**
链接到其他服务中的容器,使用服务名称或别名
links: - db - db:database - redis
别名会自动在服务器中的 /etc/hosts
里创建。例如:
172.17.2.186 db172.17.2.186 database172.17.2.187 redis**external_links**
连接到 docker-compose.yml
外部的容器,甚至并非 Compose
管理的容器。
external_links: - redis_1 - project_db_1:mysql - project_db_1:postgresql
ports
暴露端口信息 HOST:CONTAINER
格式或者仅仅指定容器的端口(宿主机会随机分配端口)
ports: - "3000" - "8000:8000" - "49100:22" - "127.0.0.1:8001:8001"
注:当使用
HOST:CONTAINER
格式来映射端口时,如果你使用的容器端口小于 60 你可能会得到错误得结果,因为YAML
将会解析xx:yy
这种数字格式为 60 进制。所以建议采用字符串格式。
**expose**
暴露端口,但不映射到宿主机,只被连接的服务访问
expose: - "3000" - "8000"
volumes
卷挂载路径设置。可以设置宿主机路径 (HOST:CONTAINER
) 或加上访问模式 (HOST:CONTAINER:ro
)。
volumes: - /var/lib/mysql - cache/:/tmp/cache - ~/configs:/etc/configs/:ro
**
**
volumes_from
从另一个服务或容器挂载它的所有卷。
volumes_from: - service_name - container_name
environment
设置环境变量。你可以使用数组或字典两种格式。
只给定名称的变量会自动获取它在 Compose 主机上的值,可以用来防止泄露不必要的数据。
environment: RACK_ENV: development SESSION_SECRET:environment: - RACK_ENV=development - SESSION_SECRET
env_file
从文件中获取环境变量,可以为单独的文件路径或列表。
如果通过 docker-compose -f FILE
指定了模板文件,则 env_file
中路径会基于模板文件路径。
如果有变量名称与 environment
指令冲突,则以后者为准。
env_file: .envenv_file: - ./common.env - ./apps/web.env - /opt/secrets.env
环境变量文件中每一行必须符合格式,支持 #
开头的注释行。
# common.env: Set Rails/Rack environmentRACK_ENV=development
extends
基于已有的服务进行扩展。例如我们已经有了一个 webapp 服务,模板文件为 common.yml
。
# common.ymlwebapp: build: ./webapp environment: - DEBUG=false - SEND_EMAILS=false
编写一个新的 development.yml
文件,使用 common.yml
中的 webapp 服务进行扩展。
# development.ymlweb: extends: file: common.yml service: webapp ports: - "8000:8000" links: - db environment: - DEBUG=truedb: image: postgres
后者会自动继承 common.yml 中的 webapp 服务及相关环节变量。
**
**
net
设置网络模式。使用和 docker client
的 --net
参数一样的值。
net: "bridge"net: "none"net: "container:[name or id]"net: "host"
**
**
pid
跟主机系统共享进程命名空间。打开该选项的容器可以相互通过进程 ID 来访问和操作。
pid: "host"
dns
配置 DNS 服务器。可以是一个值,也可以是一个列表。
dns: 8.8.8.8dns: - 8.8.8.8 - 9.9.9.9
cap_add, cap_drop
添加或放弃容器的 Linux 能力(Capabiliity)。
cap_add: - ALLcap_drop: - NET_ADMIN - SYS_ADMIN
**
**
dns_search
配置 DNS 搜索域。可以是一个值,也可以是一个列表。
dns_search: example.comdns_search: - domain1.example.com - domain2.example.com
**
**
working_dir, entrypoint, user, hostname, domainname, mem_limit, privileged, restart, stdin_open, tty, cpu_shares
这些都是和 docker run
支持的选项类似。
当使用 docker run
启动一个容器时,在后台 Docker 为容器创建一个独立的命名空间和控制集合。命名空间踢空了最基础的也是最直接的隔离,在容器中运行的进程不会被运行在主机上的进程和其他容器发现和作用。
控制组是 Linux 容器机制的另一个关键组件,负责实现资源的审计和限制。
它提供了很多特性,确保哥哥容器可以公平地分享主机的内存、CPU、磁盘IO等资源;当然,更重要的是,控制组确保了当容器内的资源使用产生压力时不会连累主机系统。
能力机制是 Linux 内核的一个强大特性,可以提供细粒度的权限访问控制。 可以作用在进程上,也可以作用在文件上。
例如一个服务需要绑定低于 1024 的端口权限,并不需要 root 权限,那么它只需要被授权 net_bind_service
能力即可。
默认情况下, Docker 启动的容器被严格限制只允许使用内核的一部分能力。
使用能力机制加强 Docker 容器的安全有很多好处,可以按需分配给容器权限,这样,即便攻击者在容器中取得了 root 权限,也不能获取宿主机较高权限,能进行的破坏也是有限的。
https://docs.docker.com/engine/reference/commandline/images/
]]>目前 3-hexo
已经集成了评论系统有 gitalk
、gitment
、 disqus
、来必力
、utteranc
gitalk 是一款基于 Github Issue 和 Preact 开发的评论插件 官网: https://gitalk.github.io/
点击进行注册 ,如下
注册完后,可得到 Client ID
和 Client Secret
因为 gitalk
是基于 Github 的 Issue 的,所以需要指定一个仓库,用来承接 gitalk 的评论,我们一般使用 Github Page 来做我们博客的评论,所以,新建仓库名为 xxx.github.io
,其中 xxx 为你的 Github 用户名
在主题下 _config.yml
中找到如下配置,启用评论,并使用 gitalk
##########评论设置#############comment: on: true type: gitalk
在主题下 _config.yml
中找到 gitalk 配置,将 第1步 得到的 Client ID
和 Client Secret
复制到如下位置
gitalk: githubID: # 填你的 github 用户名 repo: xxx.github.io # 承载评论的仓库,一般使用 Github Page 仓库 ClientID: # 第1步获得 Client ID ClientSecret: # 第1步获得 Client Secret adminUser: # Github 用户名 distractionFreeMode: true language: zh-CN perPage: 10
官网http://livere.com/ ,创建账号,点击上面的安装,选择 City 免费版
复制获取到的代码中的 data-uid
在主题下 _config.yml
在找到来必力配置如下,第一步中复制的 data-uid
粘贴到下面 data_uid
处
livere: data_uid: xxxxxx
找到以下代码, 开启并选择 livere (来必力)
##########评论设置#############comment: on: true type: livere
官网地址:https://utteranc.es/
在主题下 _config.yml
中找到 utteranc
的配置 ,修改 repo
为自己的仓库名
utteranc: repo: xxx/xxx.github.io # 承载评论的仓库,填上自己的仓库 issue_term: pathname # Issue 与 博客文章 之间映射关系 label: utteranc # 创建的 Issue 添加的标签 theme: github-light # 主题,可选主题请查看官方文档 https://utteranc.es/#heading-theme# 官方文档 https://utteranc.es/# 使用说明 https://yelog.org//2020/05/23/3-hexo-comment/
在主题下 _config.yml
中找到如下配置,启用评论,并使用 utteranc
comment: on: true type: utteranc
]]>npm install hexo-filter-mermaid-diagrams
themes/3-hexo/_config.yml
的 mermaid.on
,开启主题支持# Mermaid 支持mermaid:on: truecdn: //cdn.jsdelivr.net/npm/mermaid@8.4.2/dist/mermaid.min.js#cdn: //cdnjs.cloudflare.com/ajax/libs/mermaid/8.3.1/mermaid.min.jsoptions: # 更多配置信息可以参考 https://mermaidjs.github.io/#/mermaidAPI theme: 'default' startOnLoad: true flowchart: useMaxWidth: false htmlLabels: true
以下示例源码可以在这边查看 本文源码
更多示例可以查看官网:https://mermaidjs.github.io
graph TD; A-->B; A-->C; B-->D; C-->D;
graph TB c1-->a2 subgraph one a1-->a2 end subgraph two b1-->b2 end subgraph three c1-->c2 end
sequenceDiagram participant Alice participant Bob Alice->>John: Hello John, how are you? loop Healthcheck John->>John: Fight against hypochondria end Note right of John: Rational thoughts
prevail! John-->>Alice: Great! John->>Bob: How about you? Bob-->>John: Jolly good!
classDiagram Animal <|-- Duck Animal <|-- Fish Animal <|-- Zebra Animal : +int age Animal : +String gender Animal: +isMammal() Animal: +mate() class Duck{ +String beakColor +swim() +quack() } class Fish{ -int sizeInFeet -canEat() } class Zebra{ +bool is_wild +run() }
stateDiagram [*] --> Active state Active { [*] --> NumLockOff NumLockOff --> NumLockOn : EvNumLockPressed NumLockOn --> NumLockOff : EvNumLockPressed -- [*] --> CapsLockOff CapsLockOff --> CapsLockOn : EvCapsLockPressed CapsLockOn --> CapsLockOff : EvCapsLockPressed -- [*] --> ScrollLockOff ScrollLockOff --> ScrollLockOn : EvCapsLockPressed ScrollLockOn --> ScrollLockOff : EvCapsLockPressed }
gantt dateFormat YYYY-MM-DD title Adding GANTT diagram functionality to mermaid section A section Completed task :done, des1, 2014-01-06,2014-01-08 Active task :active, des2, 2014-01-09, 3d Future task : des3, after des2, 5d Future task2 : des4, after des3, 5d section Critical tasks Completed task in the critical line :crit, done, 2014-01-06,24h Implement parser and jison :crit, done, after des1, 2d Create tests for parser :crit, active, 3d Future task in critical line :crit, 5d Create tests for renderer :2d Add to mermaid :1d section Documentation Describe gantt syntax :active, a1, after des1, 3d Add gantt diagram to demo page :after a1 , 20h Add another diagram to demo page :doc1, after a1 , 48h section Last section Describe gantt syntax :after doc1, 3d Add gantt diagram to demo page :20h Add another diagram to demo page :48h
pie "Dogs" : 386 "Cats" : 85 "Rats" : 15]]>
前往网易云音乐官网,搜索一个作为背景音乐的歌曲,并进入播放页面,点击 生成外链播放器
设置好想要显示的样式后,复制 html 代码
最好外层在加一个 div
,如下,可直接将上一步复制的 iframe
替换下方里面的 iframe
<div id="musicMouseDrag" style="position:fixed; z-index: 9999; bottom: 0; right: 0;"> <div id="musicDragArea" style="position: absolute; top: 0; left: 0; width: 100%;height: 10px;cursor: move; z-index: 10;"></div> <iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=38592976&auto=1&height=66"></iframe></div>
将上一步加过 div
的代码粘贴到主题下 layout/_partial/footer.ejs
的最后面
默认给的样式是显示在右下角,可以通过调整上一步粘贴的 div
的 style
中 bottom
和 right
来调整位置。
如果需要自由拖动,在刚才添加的代码后面,再添加下面代码即可,鼠标就可以在音乐控件的 上边沿 点击拖动
<!--以下代码是为了支持随时拖动音乐控件的位置,如没有需求,可去掉下面代码--><script> var $DOC = $(document) $('#musicMouseDrag').on('mousedown', function (e) { // 阻止文本选中 $DOC.bind("selectstart", function () { return false; }); $('#musicDragArea').css('height', '100%'); var $moveTarget = $('#musicMouseDrag'); $moveTarget.css('border', '1px dashed grey') var div_x = e.pageX - $moveTarget.offset().left; var div_y = e.pageY - $moveTarget.offset().top; $DOC.on('mousemove', function (e) { var targetX = e.pageX - div_x; var targetY = e.pageY - div_y; targetX = targetX < 0 ? 0 : (targetX + $moveTarget.outerWidth() >= window.innerWidth) ? window.innerWidth - $moveTarget.outerWidth() : targetX; targetY = targetY < 0 ? 0 : (targetY + $moveTarget.outerHeight() >= window.innerHeight) ? window.innerHeight - $moveTarget.outerHeight() : targetY; $moveTarget.css({'left': targetX + 'px', 'top': targetY + 'px', 'bottom': 'inherit', 'right': 'inherit'}) }).on('mouseup', function () { $DOC.unbind("selectstart"); $DOC.off('mousemove') $DOC.off('mouseup') $moveTarget.css('border', 'none') $('#musicDragArea').css('height', '10px'); }) })</script>
]]>只要在在文章中使用如下关键字,不区分大小写,便可以在相应位置显示目录导航,效果文章开头
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
占位
这篇文章发表于1996年第二期《东方》杂志,同样收录于《沉默的大多数》一书中。
文章一开头就抛出了一个问题:什么是知识分子最害怕的事?想起了高晓松在晓说中提到过这个问题,晓松肯定是看过这篇文章的。
王小波说:“知识分子最怕活在不理智的年代。”所谓不理智的年代,就是伽利略低头认罪,承认地球不转的年代,也是拉瓦斯上断头台的年代;是茨威格服毒自杀的年代,也是老舍跳太平湖的年代。“
王小波和他的美国老师谈论了一个问题:”有信仰比无信仰要好。“,由于王小波是经历过文革的,所以王小波一开始是抵触这种思想的,尤其是 课间祷告 **让王小波想起了文革中的 **早请示。但老师最终说服了他,“不管是信神,还是自珍自重,人活在世界上总得有点信念才成。就我个人而言,虽是无神论者,我也有个人操守,从不逾越。”
国内的学者,只搞学术研究,不搞意识形态,这由不了自己。有朝一日它成了意识形态,你的话就是罪状。言论不自由,不理智,民族狂热,这不就是知识分子最怕的事情吗?
王小波崇拜墨子:其一,他思维缜密,其二,他敢赤裸裸地谈利害。(有了他,我也敢说自己是中华民族的赤诚分子,不怕国学家说我是全盘西化了。)
营造意识形态则是灭绝思想额丰饶。中国的人文知识分子,有种以天下为己任的使命感,总觉得自己该搞出些老百姓当信仰的东西。
国学,这种东西实在厉害。最可怕之处就在于那个“国”字。顶着这个字,谁敢有不同意见?抢到了这个制高点,就可以压制一切不同意见;所以很容易落入思想流氓的手中变成一种凶器。
目前正值 “中美贸易战”,各种自媒体为了点击量、关注度。煽动民族狂热情绪,导致民众根本容不得半点不同意见,不讲道理,“盲目爱国“。
认真思索,真诚的明辨是非,有这种态度大概就可算是善良了吧。
人活在世上,自会形成信念,一种学问、一本书,假如不对我的价值观发生变化,就不值得一学,不值得一看。
]]>在富贵的一生中,每次出现看似被上天眷顾的福气后(如有庆长跑第一、凤霞嫁了人并怀了孩子),读者还在替富贵开心的时候,他们却以各种方式迅速死去,最终富贵亲手埋葬了他所有的亲人。
一本 12w 左右的小说,但是在没有华丽词藻的情况下,在顺畅流利的写作手法、跌宕起伏的剧情、第一人称的代入感下一口气读完了。期间多次痛哭流涕(一点儿没夸张),不得不放下书本,洗过脸后才能继续阅读。所以已经多年没写书评的我,还是忍不住为她写下书评。
人是为了活着本身而活着,而不是为了活着之外的任何事物所活着。
这是作者在中文序言中的一句话,在当今生活着的我,初读序言中的这句话,并无任何共鸣,甚至还行吐槽两句。随着富贵将他的”一生”娓娓道来,你就会明白在那样的时代背景下,活着已经是一件不容易的事。 所以作者在日文版序言中说到:
在旁人眼中富贵的一生是苦熬的一生;可是对于富贵自己,我相信他更多地感受到了幸福。
因为他相信自己的妻子是世上最好的妻子,他相信自己的子女也是世上最好的子女,还有他的女婿他的外孙,还有他的那头也叫富贵的牛,还有一起上火锅的朋友们,还有生活的点点滴滴……
富贵的真是一路跌下去的一生,从”富家少爷”赌光了家产、气死了爹爹。由于母亲生病,为母亲求医路上被国民党抓壮丁,被俘虏后,放回家中。却发现母亲已死,女儿也由于生病变成了聋哑人。本想着大难之后必有后福,却只是悲惨一生的开端。儿子有庆由于和县长夫人血型匹配,遭抽血而亡、女儿凤霞产子大出血而亡、妻子家珍失去儿女后,失去了最后与病魔争斗的信念,也走了、女婿二喜在工地被水泥板拍死、外孙苦根难得吃到豆子,却被豆子撑死。最后只剩下自己和一个也叫作富贵的老牛。
春生想自杀前,找到富贵告别,在被家珍原谅,并答应不会自杀,在这种情况下坚持了一个月,最终还是自杀了。那种时代背景下的无奈,那种窒息感。。。
富贵的一生跨越了地主、解放战争、人民公社运动、大炼钢铁、自然灾害和文化大革命,从一个人的视角看到每个时代下的一个小小的缩影,但却比任何其他的描述更让人深刻了解到这些时代背景下人们的生活状态。
在那时,活着不仅是幸运,也更需要勇气。
]]>