Skywalking 中 Agent 自動同步配置源碼解析

文章目錄

    • 前言
    • 正文
      • 實現架構
      • 實現模型
      • OAP 同步 Apollo
        • ConfigWatcherRegister
        • ConfigChangeWatcher
      • Agent 側

前言

本文代碼 OAP 基于 v9.7,Java Agent 基于 v9.1,配置中心使用 apollo。

看本文需要配合代碼“食用”。

正文

Skywalking 中就使用這種模型實現了 Agent 同步Apollo 配置,本文介紹下提供的功能以及代碼實現,一起學習下。

Skywalking 支持 agent 動態更新配置,使 agent 可以依據業務需求進行自定義配置;更重要的是建立起這一個通信機制,那么 agent 的可管理性、擴展性都大大提升。

目前 Skywalking 提供了以下配置項

在這里插入圖片描述

按照文檔描述,主要為以下內容:

  • 控制采樣速率

  • 忽略指定后綴的請求,注意必須是 first span 的 opretationName 匹配到

    針對 web 服務,有些靜態資源是放在服務端,那么可以過濾掉這些請求

  • 忽略某些 path 的 trace

  • 限定每個 segment 中的 span 最大數量

  • 是否收集執行 sql 的參數

樣例配置

configurations:serviceA:trace.sample_n_per_3_secs: 1000trace.ignore_path: /a/b/c,/a1/b1/c1serviceB:trace.sample_n_per_3_secs: 1000trace.ignore_path: /a/b/c,/a1/b1/c1

注意:這個是按照服務來進行逐項配置,如果不需要變動,不要添加對應 key,會使用默認值。

實現架構

  • OAP 同步 Apollo 配置

  • Agent 同步 OAP 配置。

每階段的操作無關聯,都是作為 Client 的一端發起的請求來同步數據。

實現模型

配置動態變更實際上是一個訂閱發布模型,簡單描述就是有發布者和訂閱者兩種角色,之間交互一般是:有一個注冊接口,方便訂閱者注冊自身,以及發布者可以獲取到訂閱者列表;一個通知接口,方便發布者發送消息給訂閱者。

例如需要訂水,只要給訂水公司留下自己的電話、地址及數量(發布者知道如何找到你),之后就有人送水上門(有水時進行派送)。

這種模型理解起來很簡單,實現上難度也不大,且使用場景很廣泛。

OAP 同步 Apollo

首先看下 OAP 是如何同步 apollo 數據。

ConfigWatcherRegister

這是一個抽象類,代表的是配置中心的角色,實現上有 apollo、nacos、zk 等方式。

在這里插入圖片描述

先看下 notifySingleValue 方法:

protected void notifySingleValue(final ConfigChangeWatcher watcher, ConfigTable.ConfigItem configItem) {String newItemValue = configItem.getValue();if (newItemValue == null) {if (watcher.value() != null) {// Notify watcher, the new value is null with delete event type.// 調用 watcher 的 notify 進行處理 watcher.notify(new ConfigChangeWatcher.ConfigChangeEvent(null, ConfigChangeWatcher.EventType.DELETE));} else {// Don't need to notify, stay in null.}} else {if (!newItemValue.equals(watcher.value())) {watcher.notify(new ConfigChangeWatcher.ConfigChangeEvent(newItemValue,ConfigChangeWatcher.EventType.MODIFY));} else {// Don't need to notify, stay in the same config value.}}
}

該方法的邏輯是:讀取 configItem 中的值,并且與 watcher 中的值進行比較,不相等之后判定是 DELETE、還是 UPDATE 操作,并封裝成一個 ConfigChangeEvent 發送給 ConfigChangeWatcher,那么可以看出 ConfigChangeWatcher 是個訂閱者的角色。

繼續看下調用 notifySingleValue 方法的地方:

FetchingConfigWatcherRegister#singleConfigsSync

private final Register singleConfigChangeWatcherRegister = new Register();public abstract Optional<ConfigTable> readConfig(Set<String> keys);private void singleConfigsSync() {// 1. 讀取配置數據Optional<ConfigTable> configTable = readConfig(singleConfigChangeWatcherRegister.keys());// Config table would be null if no change detected from the implementation.configTable.ifPresent(config -> {config.getItems().forEach(item -> {// 2. 遍歷獲取配置中的 itemNameString itemName = item.getName();// 3. 依據 itemName 找到 WatcherHolderWatcherHolder holder = singleConfigChangeWatcherRegister.get(itemName);if (holder == null) {return;}ConfigChangeWatcher watcher = holder.getWatcher();// 從 WatcherHolder 得到 ConfigChangeWatcher,發送通知notifySingleValue(watcher, item);});});
}

該方法執行的邏輯就是:

  1. 依據 singleConfigChangeWatcherRegister.keys() 作為參數讀取配置信息
  2. 遍歷配置信息,依據配置中的 name(即 itemName)找到 WatcherHolder,進而獲取 ConfigChangeWatcher
  3. 調用 notifySingleValue。

readConfig 是個抽象方法,由具體的配置中心插件實現,本例中使用的 apollo,具體實現就是 ApolloConfigWatcherRegister。

讀取到的內容類型 ConfigTable,并且可以知道是存儲的 k-v 集合,那么 ConfigItem 就是每個配置項,itemName 就是 apollo 中配置的 key。

再看看調用 singleConfigsSync 的邏輯:

// FetchingConfigWatcherRegister.javapublic void start() {isStarted = true;Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(new RunnableWithExceptionProtection(this::configSync, // 啟動定時任務來執行t -> log.error("Sync config center error.", t)), 0, syncPeriod, TimeUnit.SECONDS);
}void configSync() {singleConfigsSync();groupConfigsSync();
}

再回到 singleConfigsSync 中,讀取配置時需要先獲取到配置項的 key 的集合:singleConfigChangeWatcherRegister.keys()

先看下 singleConfigChangeWatcherRegister 的具體實現:FetchingConfigWatcherRegister$Register 內部就是一個 Map<String, WatcherHolder> 來存儲。

static class Register {private Map<String, WatcherHolder> register = new HashMap<>();private boolean containsKey(String key) {return register.containsKey(key);}private void put(String key, WatcherHolder holder) {register.put(key, holder);}public WatcherHolder get(String name) {return register.get(name);}public Set<String> keys() {return register.keySet();}
}

有讀取就有存儲,看看調用 put 邏輯:

// FetchingConfigWatcherRegister
synchronized public void registerConfigChangeWatcher(ConfigChangeWatcher watcher) {WatcherHolder holder = new WatcherHolder(watcher);if (singleConfigChangeWatcherRegister.containsKey(holder.getKey()) || groupConfigChangeWatcherRegister.containsKey(holder.getKey())) {}switch (holder.getWatcher().getWatchType()) {case SINGLE:// put 調用singleConfigChangeWatcherRegister.put(holder.getKey(), holder);break;case GROUP:groupConfigChangeWatcherRegister.put(holder.getKey(), holder);break;default:}
}

registerConfigChangeWatcher 方法,用于注冊 ConfigChangeWatcher ,內部處理邏輯:先將 watcher 放入 watchHolder 中,再以 holder key 分開存儲 holder(放入 FetchingConfigWatcherRegister$Register 中)。

WatcherHolder 是 ConfigWatcherRegister 一個內部類,代碼如下,重點是 key 生成規則:String.join(".", watcher.getModule(), watcher.getProvider().name(), watcher.getItemName());,每個 itemName 對應一個 watcher。

@Getter
protected static class WatcherHolder {private ConfigChangeWatcher watcher;private final String key;public WatcherHolder(ConfigChangeWatcher watcher) {this.watcher = watcher;this.key = String.join(".", watcher.getModule(), watcher.getProvider().name(),watcher.getItemName());}
}

總結:OAP 啟動定時任務,同步 apollo 的配置數據,遍歷每個配置項(configItem),找到對應的 ConfigChangerWater,將 watcher 中的值與 configItem 中的值進行比較,不相等之后繼續判定是 DELETE、還是 UPDATE 操作,封裝成一個 ConfigChangeEvent 發送給對應的 ConfigChangeWatcher。

ConfigChangeWatcher

抽象類,依據命名,表示的是關注配置變化的 watcher,是 OAP 中定義的用于對不同配置的具體實現;對于 Apollo 上的每個 Key 都有對應的 ConfigChangeWatcher。

在這里插入圖片描述

具體的 ConfigChangeWatcher 獲取到 ConfigChangeEvent,處理邏輯各有不同,本次具體看下 AgentConfigurationsWatcher。

private volatile String settingsString;private volatile AgentConfigurationsTable agentConfigurationsTable;public void notify(ConfigChangeEvent value) {if (value.getEventType().equals(EventType.DELETE)) {settingsString = null;this.agentConfigurationsTable = new AgentConfigurationsTable();} else {settingsString = value.getNewValue();AgentConfigurationsReader agentConfigurationsReader =new AgentConfigurationsReader(new StringReader(value.getNewValue()));this.agentConfigurationsTable = agentConfigurationsReader.readAgentConfigurationsTable();}
}

方法邏輯為:config value 存儲到了 agentConfigurationsTable。

apollo value 是什么樣子呢?

configurations:serviceA:trace.sample_n_per_3_secs: 1000trace.ignore_path: /a/b/c,/a1/b1/c1serviceB:trace.sample_n_per_3_secs: 1000trace.ignore_path: /a/b/c,/a1/b1/c1

AgentConfigurationsTable 如下具體實現

public class AgentConfigurationsTable {private Map<String, AgentConfigurations> agentConfigurationsCache;public AgentConfigurationsTable() {this.agentConfigurationsCache = new HashMap<>();}
}public class AgentConfigurations {private String service;private Map<String, String> configuration;/*** The uuid is calculated by the dynamic configuration of the service.*/private volatile String uuid;public AgentConfigurations(final String service, final Map<String, String> configuration, final String uuid) {this.service = service;this.configuration = configuration;this.uuid = uuid;}
}

將 agentConfigurationsTable 轉換成 json 展示更容里理解數據存儲的結構:

{"serviceB": {"service": "serviceB","configuration": {"trace.sample_n_per_3_secs": "1000","trace.ignore_path": "/a/b/c,/a1/b1/c1"},"uuid": "92670f1ccbdee60e14ffc0"},"serviceA": {"service": "serviceA","configuration": {"trace.sample_n_per_3_secs": "1000","trace.ignore_path": "/a/b/c,/a1/b1/c1"},"uuid": "92670f1ccbdee60e14ffc0"}
}

查看讀取 agentConfigurationsTable 值的邏輯:

// AgentConfigurationsWatcher#getAgentConfigurations
public AgentConfigurations getAgentConfigurations(String service) {// 依據 service 獲取數據AgentConfigurations agentConfigurations = this.agentConfigurationsTable.getAgentConfigurationsCache().get(service);if (null == agentConfigurations) {return emptyAgentConfigurations;} else {return agentConfigurations;}
}

繼續查看調用 getAgentConfigurations 的代碼,并且將 value 包裝成 ConfigurationDiscoveryCommand 返回。

// ConfigurationDiscoveryServiceHandler#fetchConfigurations
public void fetchConfigurations(final ConfigurationSyncRequest request,final StreamObserver<Commands> responseObserver) {Commands.Builder commandsBuilder = Commands.newBuilder();AgentConfigurations agentConfigurations = agentConfigurationsWatcher.getAgentConfigurations(request.getService());if (null != agentConfigurations) {// 請求時會帶有 uuid,會跟現有配置的 uuid 進行比對,如果不同,則獲取最新值 if (disableMessageDigest || !Objects.equals(agentConfigurations.getUuid(), request.getUuid())) {ConfigurationDiscoveryCommand configurationDiscoveryCommand =newAgentDynamicConfigCommand(agentConfigurations);commandsBuilder.addCommands(configurationDiscoveryCommand.serialize().build());}}responseObserver.onNext(commandsBuilder.build());responseObserver.onCompleted();
}

ConfigurationDiscoveryServiceHandler 屬于 GRPCHandler,類似 SpringBoot 中 Controller,暴露接口,外部就可以獲取數據。

ConfigurationDiscoveryCommand 這個方法中有個屬性來標識 command 的具體類型,這個在 agent 端接收到 command 需要依據 command 類型找到真正的處理器。

public static final String NAME = "ConfigurationDiscoveryCommand";

總結:當 AgentConfigurationsWatcher 收到訂閱的 ConfigChangeEvent 時,會將值存儲至 AgentConfigurationsTable,之后通過 ConfigurationDiscoveryServiceHandler 暴露接口,以方便 agent 可以獲取到相應服務的配置。

至此,OAP 與 Apollo 間的配置更新邏輯以及值的處理邏輯大致理清了。

接下來看看 agent 與 oap 間的交互。

Agent 側

找到調用 ConfigurationDiscoveryServiceGrpc#fetchConfigurations 的代碼,看到 ConfigurationDiscoveryService,查看具體調用邏輯:

// ConfigurationDiscoveryService
private void getAgentDynamicConfig() {if (GRPCChannelStatus.CONNECTED.equals(status)) {try {// 準備參數ConfigurationSyncRequest.Builder builder = ConfigurationSyncRequest.newBuilder();builder.setService(Config.Agent.SERVICE_NAME);if (configurationDiscoveryServiceBlockingStub != null) {final Commands commands = configurationDiscoveryServiceBlockingStub.withDeadlineAfter(GRPC_UPSTREAM_TIMEOUT, TimeUnit.SECONDS).fetchConfigurations(builder.build()); // 方法調用// 結果處理ServiceManager.INSTANCE.findService(CommandService.class).receiveCommand(commands);}} catch (Throwable t) {}}
}

而 getAgentDynamicConfig 是在 ConfigurationDiscoveryService#boot 執行時 init 了一個定時任務調用。

public void boot() throws Throwable {getDynamicConfigurationFuture = Executors.newSingleThreadScheduledExecutor(new DefaultNamedThreadFactory("ConfigurationDiscoveryService")).scheduleAtFixedRate(new RunnableWithExceptionProtection(this::getAgentDynamicConfig,t -> LOGGER.error("Sync config from OAP error.", t)),Config.Collector.GET_AGENT_DYNAMIC_CONFIG_INTERVAL,Config.Collector.GET_AGENT_DYNAMIC_CONFIG_INTERVAL,TimeUnit.SECONDS);
}

獲取結果后的處理邏輯:CommandService 接收 Commands,先是放入到隊列中,

private LinkedBlockingQueue<BaseCommand> commands = new LinkedBlockingQueue<>(64);public void receiveCommand(Commands commands) {for (Command command : commands.getCommandsList()) {try {BaseCommand baseCommand = CommandDeserializer.deserialize(command);// 將結果放入隊列中boolean success = this.commands.offer(baseCommand);if (!success && LOGGER.isWarnEnable()) {}} catch (UnsupportedCommandException e) {}}
}

新開線程來消費隊列,commandExecutorService 處理 Commands,通過代碼調用鏈看到,最后依據 command 的類型找到真正指令執行器。

// CommandService#run
public void run() {final CommandExecutorService commandExecutorService = ServiceManager.INSTANCE.findService(CommandExecutorService.class);while (isRunning) {try {// 消費隊列BaseCommand command = this.commands.take();// 判斷是否已經執行過了if (isCommandExecuted(command)) {continue;}// 分發 commandcommandExecutorService.execute(command);serialNumberCache.add(command.getSerialNumber());} catch (CommandExecutionException e) {}}
}// CommandExecutorService#execute
public void execute(final BaseCommand command) throws CommandExecutionException {this.executorForCommand(command).execute(command);
}
// CommandExecutorService#executorForCommand
private CommandExecutor executorForCommand(final BaseCommand command) {final CommandExecutor executor = this.commandExecutorMap.get(command.getCommand());if (executor != null) {return executor;}return NoopCommandExecutor.INSTANCE;
}

依據指令類型獲取具體的指令執行器,這里為 ConfigurationDiscoveryService,發現又調用了 ConfigurationDiscoveryService#handleConfigurationDiscoveryCommand 處理。

// ConfigurationDiscoveryService#handleConfigurationDiscoveryCommand
public void handleConfigurationDiscoveryCommand(ConfigurationDiscoveryCommand configurationDiscoveryCommand) {final String responseUuid = configurationDiscoveryCommand.getUuid();List<KeyStringValuePair> config = readConfig(configurationDiscoveryCommand);// 遍歷配置項config.forEach(property -> {String propertyKey = property.getKey();List<WatcherHolder> holderList = register.get(propertyKey);for (WatcherHolder holder : holderList) {if (holder != null) {// 依據配置項找到對應的 AgentConfigChangeWatcher,封裝成 ConfigChangeEvent AgentConfigChangeWatcher watcher = holder.getWatcher();String newPropertyValue = property.getValue();if (StringUtil.isBlank(newPropertyValue)) {if (watcher.value() != null) {// Notify watcher, the new value is null with delete event type.watcher.notify(new AgentConfigChangeWatcher.ConfigChangeEvent(null, AgentConfigChangeWatcher.EventType.DELETE));}} else {if (!newPropertyValue.equals(watcher.value())) {watcher.notify(new AgentConfigChangeWatcher.ConfigChangeEvent(newPropertyValue, AgentConfigChangeWatcher.EventType.MODIFY));}}}}});this.uuid = responseUuid;
}

ConfigurationDiscoveryService#handleConfigurationDiscoveryCommand 進行處理,遍歷配置項列表,依據 Key 找到對應的 AgentConfigChangeWatcher,進行 notify。

這個過程是不是很熟悉,跟 OAP 中處理邏輯不能說是完全一樣,簡直一模一樣。

AgentConfigChangeWatcher 是個抽象類,查看其具體實現,關注其注冊以及處理 value 的邏輯即可。
在這里插入圖片描述

具體邏輯就不再展開細說了,需要自行了解下。

總之,agent 可以進行動態配置,能做的事情就多了,尤其是對 agent.config 中的配置大部分就可以實現動態管理了。

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

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

相關文章

華為5720設置靜態路由不通_【干貨分享】交換機與路由器在環路中的處理機制了解一下!...

點擊藍字關注我們-今天小盟帶大家來討論一下交換機與路由器在環路中的處理機制-01基礎配置1---如圖配置路由器各接口地址&#xff0c;AR-2為PC-1的網關路由器2---AR-1配置靜態默認路由&#xff0c;下一跳地址指向AR-2&#xff1b;[AR-1]ip route-static 0.0.0.0 0 12.1.1.2AR-2…

IPC 進程間通信方式——信號量

信號量 本質上是共享資源的數目&#xff0c;用來控制對共享資源的訪問。用于進程間的互斥和同步每種共享資源對應一個信號量&#xff0c;為了便于大量共享資源的操作引入了信號量集&#xff0c;可對多對信號量一次性操作。對信號量集中所有的操作可以要求全部成功&#xff0c;也…

css選擇器的優先級

選擇器的優先級表述為4個部分&#xff0c;用0,0,0,0表示。 !important--1,0,0,0行內樣式ID選擇器--0,1,0,0類選擇器(例如,.example)、屬性選擇器&#xff08;例如, [type"radio"]&#xff09;或偽類&#xff08;例如, :hover&#xff09;--0,0,1,0元素&#xff08;例…

VisualVM介紹使用

1 打開VisualVM&#xff08;這個工具放在JDK安裝目錄的bin目錄下&#xff0c;雙擊jvisualvm.exe即可打開&#xff09;&#xff0c;如下圖所示 以VisualVM自身為例&#xff0c;VisualVM本身也是一個java程序&#xff0c;當然也而已用VisualVM來分析 2 概述頁面主要顯示程序…

c語言奇葩錯誤,6個奇葩的(hello,world)C語言版(轉)

//下面的所有程序都可以在GCC下編譯通過&#xff0c;只有最后一個需要動用C的編譯器用才能編譯通過。//程序功能輸出 Hello,world!01.c#define _________ }#define ________ putchar#define _______ main#define _(a) ________(a);#define ______ _______(){#define __ _____…

Java功能的適用性

Java語言和標準庫功能強大&#xff0c;但功能強大&#xff0c; 責任重大 。 一方面看到很多用戶代碼濫用或濫用稀有的Java功能&#xff0c;另一方面卻完全忘記了大多數基本功能之后&#xff0c;我決定撰寫此摘要。 這不是每個Java開發人員都應該探索&#xff0c;了解和使用的要…

臺達b3伺服modbus通訊_【數控系統】臺達伺服壓機控制靈活 精準壓合滿足各種工序需求...

引言壓機是一種利用壓力改變工件形狀的機械設備。隨著制造業少量多樣與客制化的日趨發展&#xff0c;壓機的的優勢逐漸顯現&#xff0c;在汽車、五金與電子制造等產業中的應用不斷增多。傳統壓機在使用操作上耗費人力并需要諸多壓機元件才能完整運作&#xff0c;維修成本高&…

Binary Agents(二進制值轉換字符串)

題目&#xff1a; 傳入二進制字符串&#xff0c;翻譯成英語句子并返回。 二進制字符串是以空格分隔的。 代碼&#xff1a; 1 function binaryAgent(str) {2 var arr str.split( );3 for (var i 0; i < arr.length; i) {4 arr.splice(i,1,String.fromCharCode(BtoD…

我對CSS選擇器的認識

我對CSS選擇器的認識 一、簡述   CSS選擇器是對HTML元素進行選擇的篩選條件&#xff0c;大概可以分為兩類&#xff1a; 特征選擇器——根據元素自身所具有的某種特征進行篩選&#xff0c;比如名稱、ID、屬性等&#xff1b;關系選擇器——根據元素與其他元素的關系進行篩選&…

【USACO2006 Mar】滑雪纜車 skilift

【USACO2006 Mar】 滑雪纜車 skilift Time Limit 1000 msMemory Limit 131072 KBytes Description 科羅拉多州的羅恩打算為奶牛建造一個滑雪場&#xff0c;為此要在山上規劃一條纜車線路。 整座山可以用一條折線來描述&#xff0c;該折線有N個拐點&#xff0c;起點是1&#xff…

yolov4Linux,基于Darknet的YOLOv4目標檢測

目錄一、Windows環境下的YOLOv4目標檢測1、環境配置環境準備&#xff1a;Win10、CUDA10.1、cuDNN7.65、Visual Studio 2019、OpenCV 3.4(1)Visual Studio2019企業版安裝(3)下載并安裝CUDA10.1&#xff0c;下載安裝cuDNN7.65對于cudnn直接將其解開壓縮包&#xff0c;然后需要將b…

二元置信橢圓r語言_醫學統計與R語言:圓形樹狀圖(circular dendrogram)

微信公眾號&#xff1a;醫學統計與R語言如果你覺得對你有幫助&#xff0c;歡迎轉發輸入1&#xff1a; "ggraph")結果1&#xff1a; name 輸入2&#xff1a; <- graph_from_data_frame(myedges1, verticesmyvertices,directed T)ggraph(mygraph, layout dend…

Java:檢查器框架

我在JavaOne 2012上 了解的有趣的工具之一是Checker Framework 。 Checker Framework的網頁之一 指出 &#xff0c;Checker Framework“增強了Java的類型系統&#xff0c;使其更強大&#xff0c;更有用”&#xff0c;從而使軟件開發人員“能夠檢測并防止Java程序中的錯誤”。 查…

南岸焊接機器人廠_造船三部高效焊接工藝技術年鑒

為了提升公司高效焊自動化率&#xff0c;實現降本增效目標&#xff0c;造船事業三部積極響應公司領導號召&#xff0c;充分挖掘自身資源&#xff0c;2020年&#xff0c;在高效焊接技術、設備開發研究等方面&#xff0c;不斷創新、敢于突破&#xff0c;獲取了多項焊接新技術、新…

軟工Hello World!團隊第二周博客匯總

2017.10.20-2017.10.26 Scrum會議&#xff1a; 第一天&#xff1a;http://www.cnblogs.com/liusx0303/p/7704482.html 第二天&#xff1a;http://www.cnblogs.com/Mingezi/p/7709472.html 第三天&#xff1a;http://www.cnblogs.com/lynlyn/p/7717275.html 第四天&#xff1a;h…

什么是css sprites,如何使用?

css sprites&#xff1a;精靈圖&#xff08;雪碧圖&#xff09;&#xff1a;把一堆小圖片整合在一張大圖上&#xff0c;通過背景圖片相關設置&#xff08;背景圖片、背景圖是否重復、背景圖定位&#xff09;&#xff0c;顯示圖片&#xff0c;減輕服務器對圖片的請求數量 優點&…

線性回歸csv數據集_測試數據科學家線性回歸的30個問題

你的目標是數據科學家嗎&#xff1f;那你對線性回歸了解有多深入呢&#xff0c;下面的30道題&#xff0c;可以幫助你或者測試別人是否真的達到的數據科學家的水平&#xff0c;關注回復&#xff0c;答案在評論區&#xff1a;1)是非題&#xff1a;線性回歸是一種受監督的機器學習…

linux調試crontab,linux - crontab 的調試,啟動thin服務器

linux - crontab 的調試&#xff0c;啟動thin服務器2018-11-18 17:10訪問量: 1059分類&#xff1a;技術參考&#xff1a;https://askubuntu.com/questions/56683/where-is-the-cron-crontab-log日志默認位置在 /var/log/syslog 中。 grep CRON 。 如果沒有安裝MTA的話(例如 mai…

番石榴前提條件課

編寫過很多Java文章的人可能都編寫了以條件為開頭的方法&#xff0c;這些條件可以在繼續進行該方法的其余實現之前&#xff0c;先驗證提供的參數或要操作的對象的狀態。 這些會增加方法的冗長性&#xff0c;有時&#xff0c;尤其是在有多個檢查的情況下&#xff0c;幾乎會淹沒該…

dw空心圓項目符號_如何懂建筑施工圖?搞懂這些符號解析,耐下性子研究不會學不會...

施工圖紙一個建筑方向&#xff0c;是房屋建筑的依據&#xff0c;更是一種工程語言&#xff0c;它能夠明確的規定出我們建造出怎樣的建筑&#xff0c;看懂它是入行基礎。當然建筑圖包含的因素比較多&#xff0c;有具體的建筑符號&#xff0c;尺寸、做法以及技術要求都在里面&…