分類: 3C資訊

  • 分佈式事務

    分佈式事務

    分佈式事務

    分佈式環境下的事務

    要了解分佈式事務,首先要了解分佈式環境

    分佈式

    如一網站,訪問一個服務A(查詢自己用戶信息), 提供服務A的服務器分別有A1(上海)A2(廣州) A3(新加坡)

    同一個服務分佈在三個區域的服務器上,這就是分佈式。你可以訪問 上海的服務器,廣州的或者新加坡的,但是

    三個服務器之前通信是有延遲的,所以數據同步需要一定時間

    分佈式中的問題

    舉例一個分佈式場景

    如果用戶Y個人信息 名字為 “南柯一夢” Y改為 “南柯夢”, 同一時間,Y用戶好友查看Y的名字,好友查詢的結果是”南柯一夢” 還是 “南柯夢” 這是分佈式系統常見的問題(數據修改發生在上海,訪問發生在新加坡)。

    CAP原則

    CAP原則又稱CAP定理,指的是在一個分佈式系統中,一致性(Consistency)、可用性(Availability)、分區容錯性(Partition tolerance)。CAP 原則指的是,這三個要素最多只能同時實現兩點,不可能三者兼顧。

    參考文章
    An Illustrated Proof of the CAP Theorem
    CAP 定理的含義

    以為網絡有延遲和不可測故障,因此分佈式系統是保持服務穩定的常用手段,但是分佈式因為服務機器分佈在不同地點,因此也會有分佈式的特點問題。

    分佈式中 一致性 可用性 容災性 是三個指標

    Consistency

    數據一致性,分佈式環境下,不同地點的服務器,數據庫數據同步一致。

    Availability

    服務可用性,分佈式環境下,調用服務,都可用

    Partition tolerance

    分區容錯性,容災能力

    分佈式環境多台服務器運行,其中一部分機器故障了,整個系統仍然可以正常運行提供服務

    CAP不能同時滿足

    必須滿足P

    首先分佈式環境,系統需要穩定運行,一台服務器意外斷電,不應該影響系統整體功能正常,另一台或多台服務器還能穩定提供服務,所以分區容錯是必須要滿足。

    滿足C

    數據一致性,所指的是同一個服務所在不同服務器的數據是同步的。如上改名字的場景 南柯一夢 改為 南柯夢 (在上海的數據庫被修改) 那麼系統要做到滿足數據一致性,必須馬上同步廣州和新加坡的數據庫,這樣才能滿足廣州或者新加坡的訪問者獲得的結果也一致是 “南柯夢” 而不是”南柯一夢”

    滿足A

    服務可用性,指任何時候訪問服務,都返回結果

    A與C是衝突的,上海服務器南柯一夢改為南柯夢后,為了服務可用,此時間訪問新加坡和廣州的服務器,返回的結果應該是南柯一夢(任何時候服務都返回結果) 但是嚴格上講,數據是錯誤的,因為用戶已經改了名字,改為南柯夢,但是數據在上海的才是正確的。

    滿足數據一致性必須犧牲服務可用性 或者相反

    要達到數據一致性的要求,必須在上海服務器修改數據的同時,同步廣州和新加坡的數據庫,並且在數據同步完成之前,訪問廣州和新加坡的數據庫中這條數據需要等待,返回同步后的結果(一致性)。

    失去了服務可用性(這裏服務是等待數據同步完成才返回結果,而不是立刻返回)

    因此CAP 要麼 滿足AP (分區服務可用)要麼 CP (分區數據一致)

    分佈式中事務

    商品購買中的事務

    以商品購買生成訂單為例子

    網絡上用戶A 購買 一雙鞋子 價格50 付款後生成消費訂單

    事務中包含子的服務

    這裏簡單設為三個服務,他們是事務相關的

    1.商品信息服務

    提供商品信息等服務

    鞋子 顏色 價格 庫存數量等信息 這裏設 價格price為 50 庫存數 num 9

    2.商家賬號收款服務

    提供金額收入信息等服務

    用戶購買鞋子,需要付款50元到商家賬號

    3.用戶消費訂單服務

    提供購買消費憑證信息等服務

    首先分析用戶購買鞋子,三個服務分別要做什麼

    @1 鞋子庫存減1

    @2 商家賬號金額增加50

    @3 生成 用戶購買鞋子的訂單記錄, 包括數量金額等信息

    事務特性

    原子性

    @1 @2 @3 要麼同時發生,要麼都不發生

    一致性

    鞋子庫存減少1,收入增加50

    隔離性

    鞋子庫存減1,後續用戶最多只能購買(9-1=8)雙鞋子

    持久性

    動作執行成功后,訂單生效,收入新增50生效,庫存減1生效

    上述三個服務他們可以在不同的地點,不同機器上部署的,並很常見。

    保證數據正確

    開啟事務

    確定要執行的服務,每個服務的數據庫事務開啟

    執行業務

    調用庫存減1,轉賬,生成訂單等子服務

    提交

    業務執行過程中沒有意外,各子服務的數據庫提交事務,生效數據修改

    回退

    回退,如果服務調用出現了差錯,或者某個子服務執行失敗,可以通過回滾所有數據庫達到數據正確。

    補償

    某些情況下,某個子服務執行失敗,但是不影響整體業務,也可以提交事務,後續補償機制將失敗的子服務重新執行。

    補償機制

    個人認為就商品購買而言,補償機制多數情況可以使用且實用。(對強一致要求沒那麼高的情況下)

    @1 庫存減1

    @2 收入增加50

    @ 3生成訂單記錄

    如果這次執行的動作, 只有@3失敗,@1 @2成功 說明金額交易,商品庫存業務都沒問題,只是訂單記錄失敗,這是可以提交事務的,訂單錯誤可以生成一條記錄(攜帶商品,金額等信息),發送到MQ消息隊列(或者其他設計)通過消息隊列通知訂單相關服務,補償重新執行生成訂單,達到最終一致性。

    分佈式事務控制問題

    不同服務在不同區運行

    不管是從安全性,穩定性,還是服務粒度細化方便維護等多因素考慮,都是很有必要讓不同的服務分開在不同服務區運行。

    單體數據庫的事務不被支持,購買商品到生成訂單所有操作加起來算一個事務,涉及的數據在不同一服務(不同的數據庫),並且同一個服務可能運行在多台服務器上。

    數據庫開啟事務針對的是單台服務器,多個服務多個數據庫,並不支持數據庫的事務,需要額外設計處理數據一致性問題(或者最終一致性)

    同一個服務運行在多個區

    不同服務不在一個服務器,同樣的,分佈式為穩定性可用而生,因此,一個服務大多有在多個區的服務器上運行,開啟事務的時候,如何保證事務開啟提交等事務相關命令每次發送到同一個區的同一個服務器,也是一定要考慮的問題。

    分佈式事務處理方式

    如上所述分佈式服務代表多個數據庫,不支持數據庫的事務,

    如何保證事務中涉及的數據庫數據修改都提交生效或者都回滾。

    建立控制中心

    控制中心在執行業務時,統一發送開始事務的命令給三個服務,返回狀態

    狀態沒問題執行數據修改,

    都沒問題就發送給三個服務,提交事務,否在回滾事務

    消息機制事務

    MQ消息隊列,達到控制事務正確目的,項目中kafka聽的比較多,可在高併發環境下穩定運行,可以通過消息機制發送事務處理結果到子服務,子服務收到消息,通過分析消息內容,做出對應的操作,達到事務一致性或者最終一致性等目的
    思考圖:

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • 一時技癢,擼了個動態線程池,源碼放Github了

    一時技癢,擼了個動態線程池,源碼放Github了

    闡述背景

    線程池在日常工作中用的還挺多,當需要異步,批量處理一些任務的時候我們會定義一個線程池來處理。

    在使用線程池的過程中有一些問題,下面簡單介紹下之前遇到的一些問題。

    場景一:實現一些批量處理數據的功能,剛開始線程池的核心線程數設的比較小,然後想調整下,只能改完后重啟應用。

    場景二:有一個任務處理的應用,會接收 MQ 的消息進行任務的處理,線程池的隊列也允許緩存一定數量的任務。當任務處理的很慢的時候,想看看到底有多少沒有處理完不是很方便。當時為了快速方便,就直接啟動了一個線程去循環打印線程池隊列的大小。

    正好之前在我公眾號有轉發過美團的一篇線程池應用的文章(https://mp.weixin.qq.com/s/tIWAocevZThfbrfWoJGa9w),覺得他們的思路非常好,就是沒有開放源碼,所以自己就抽時間在我的開源項目 Kitty 中增加了一個動態線程池的組件,支持了 Cat 監控,動態變更核心參數,任務堆積告警等。今天就給大家分享一下實現的方式。

    項目源代碼地址:https://github.com/yinjihuan/kitty

    使用方式

    添加依賴

    依賴線程池的組件,目前 Kitty 未發布,需要自己下載源碼 install 本地或者私有倉庫。

    <dependency>
        <groupId>com.cxytiandi</groupId>
        <artifactId>kitty-spring-cloud-starter-dynamic-thread-pool</artifactId>
    </dependency>
    

    添加配置

    然後在 Nacos 配置線程池的信息,我的這個整合了 Nacos。推薦一個應用創建一個單獨的線程池配置文件,比如我們這個叫 dataId 為 kitty-cloud-thread-pool.properties,group 為 BIZ_GROUP。

    內容如下:

    kitty.threadpools.nacosDataId=kitty-cloud-thread-pool.properties
    kitty.threadpools.nacosGroup=BIZ_GROUP
    kitty.threadpools.accessToken=ae6eb1e9e6964d686d2f2e8127d0ce5b31097ba23deee6e4f833bc0a77d5b71d
    kitty.threadpools.secret=SEC6ec6e31d1aa1bdb2f7fd5eb5934504ce09b65f6bdc398d00ba73a9857372de00
    kitty.threadpools.owner=尹吉歡
    kitty.threadpools.executors[0].threadPoolName=TestThreadPoolExecutor
    kitty.threadpools.executors[0].corePoolSize=4
    kitty.threadpools.executors[0].maximumPoolSize=4
    kitty.threadpools.executors[0].queueCapacity=5
    kitty.threadpools.executors[0].queueCapacityThreshold=5
    kitty.threadpools.executors[1].threadPoolName=TestThreadPoolExecutor2
    kitty.threadpools.executors[1].corePoolSize=2
    kitty.threadpools.executors[1].maximumPoolSize=4
    

    nacosDataId,nacosGroup

    監聽配置修改的時候需要知道監聽哪個 DataId,值就是當前配置的 DataId。

    accessToken,secret

    釘釘機器人的驗證信息,用於告警。

    owner

    這個應用的負責人,告警的消息中會显示。

    threadPoolName

    線程池的名稱,使用的時候需要關注。

    剩下的配置就不一一介紹了,跟線程池內部的參數一致,還有一些可以查看源碼得知。

    注入使用

    @Autowired
    private DynamicThreadPoolManager dynamicThreadPoolManager;
    dynamicThreadPoolManager.getThreadPoolExecutor("TestThreadPoolExecutor").execute(() -> {
        log.info("線程池的使用");
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "getArticle");
    

    通過 DynamicThreadPoolManager 的 getThreadPoolExecutor 方法獲取線程池對象,然後傳入 Runnable,Callable 等。第二個參數是這個任務的名稱,之所以要擴展一個參數是因為如果任務沒有標識,那麼無法區分任務。

    這個線程池組件默認集成了 Cat 打點,設置了名稱可以在 Cat 上查看這個任務相關的監控數據。

    擴展功能

    任務執行情況監控

    在 Cat 的 Transaction 報表中會以線程池的名稱為類型显示。

    詳情中會以任務的名稱显示。

    核心參數動態修改

    核心參數目前只支持 corePoolSize,maximumPoolSize,queueCapacity(隊列類型為 LinkedBlockingDeque 才可以修改),rejectedExecutionType,keepAliveTime,unit 這些參數的修改。

    一般 corePoolSize,maximumPoolSize,queueCapacity 是最常要動態改變的。

    需要改動的話直接在 Nacos 中將對應的配置值修改即可,客戶端會監聽配置的修改,然後同步修改先線程池的參數。

    隊列容量告警

    queueCapacityThreshold 是隊列容量告警的閥值,如果隊列中的任務數量超過了 queueCapacityThreshold 就會告警。

    拒絕次數告警

    當隊列容量滿了后,新進來的任務會根據用戶設置的拒絕策略去選擇對應的處理方式。如果是採用 AbortPolicy 策略,也會進行告警。相當於消費者已經超負荷了。

    線程池運行情況

    底層對接了 Cat,所以將線程的運行數據上報給了 Cat。我們可以在 Cat 中查看這些信息。

    如果你想在自己的平台去展示,我這邊暴露了/actuator/thread-pool 端點,你可以自行拉取數據。

    {
    	threadPools: [{
    		threadPoolName: "TestThreadPoolExecutor",
    		activeCount: 0,
    		keepAliveTime: 0,
    		largestPoolSize: 4,
    		fair: false,
    		queueCapacity: 5,
    		queueCapacityThreshold: 2,
    		rejectCount: 0,
    		waitTaskCount: 0,
    		taskCount: 5,
    		unit: "MILLISECONDS",
    		rejectedExecutionType: "AbortPolicy",
    		corePoolSize: 4,
    		queueType: "LinkedBlockingQueue",
    		completedTaskCount: 5,
    		maximumPoolSize: 4
    	}, {
    		threadPoolName: "TestThreadPoolExecutor2",
    		activeCount: 0,
    		keepAliveTime: 0,
    		largestPoolSize: 0,
    		fair: false,
    		queueCapacity: 2147483647,
    		queueCapacityThreshold: 2147483647,
    		rejectCount: 0,
    		waitTaskCount: 0,
    		taskCount: 0,
    		unit: "MILLISECONDS",
    		rejectedExecutionType: "AbortPolicy",
    		corePoolSize: 2,
    		queueType: "LinkedBlockingQueue",
    		completedTaskCount: 0,
    		maximumPoolSize: 4
    	}]
    }
    

    自定義拒絕策略

    平時我們使用代碼創建線程池可以自定義拒絕策略,在構造線程池對象的時候傳入即可。這裏由於創建線程池都被封裝好了,我們只能在 Nacos 配置拒絕策略的名稱來使用對應的策略。默認是可以配置 JDK 自帶的 CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy 這四種。

    如果你想自定義的話也是支持的,定義方式跟以前一樣,如下:

    @Slf4j
    public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            log.info("進來了。。。。。。。。。");
        }
    }
    

    要讓這個策略生效的話使用的是 SPI 的方式,需要在 resources 下面創建一個 META-INF 的文件夾,然後創建一個 services 的文件夾,再創建一個 java.util.concurrent.RejectedExecutionHandler 的文件,內容為你定義的類全路徑。

    自定義告警方式

    默認是內部集成了釘釘機器人的告警方式,如果你不想用也可以將其關閉。或者將告警信息對接到你的監控平台去。

    如果沒有告警平台也可以在項目中實現新的告警方式,比如短信等。

    只需要實現 ThreadPoolAlarmNotify 這個類即可。

    /**
     * 自定義短信告警通知
     *
     * @作者 尹吉歡
     * @個人微信 jihuan900
     * @微信公眾號 猿天地
     * @GitHub https://github.com/yinjihuan
     * @作者介紹 http://cxytiandi.com/about
     * @時間 2020-05-27 22:26
     */
    @Slf4j
    @Component
    public class ThreadPoolSmsAlarmNotify implements ThreadPoolAlarmNotify {
        @Override
        public void alarmNotify(AlarmMessage alarmMessage) {
            log.info(alarmMessage.toString());
        }
    }
    

    代碼實現

    具體的就不講的很細了,源碼在https://github.com/yinjihuan/kitty/tree/master/kitty-dynamic-thread-pool,大家自己去看,並不複雜。

    創建線程池

    根據配置創建線程池,ThreadPoolExecutor 是自定義的,因為需要做 Cat 埋點。

    /**
     * 創建線程池
     * @param threadPoolProperties
     */
    private void createThreadPoolExecutor(DynamicThreadPoolProperties threadPoolProperties) {
        threadPoolProperties.getExecutors().forEach(executor -> {
            KittyThreadPoolExecutor threadPoolExecutor = new KittyThreadPoolExecutor(
                    executor.getCorePoolSize(),
                    executor.getMaximumPoolSize(),
                    executor.getKeepAliveTime(),
                    executor.getUnit(),
                    getBlockingQueue(executor.getQueueType(), executor.getQueueCapacity(), executor.isFair()),
                    new KittyThreadFactory(executor.getThreadPoolName()),
                    getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()), executor.getThreadPoolName());
            threadPoolExecutorMap.put(executor.getThreadPoolName(), threadPoolExecutor);
        });
    }
    

    刷新線程池

    首先需要監聽 Nacos 的修改。

    /**
     * 監聽配置修改,spring-cloud-alibaba 2.1.0版本不支持@NacosConfigListener的監聽
     */
    public void initConfigUpdateListener(DynamicThreadPoolProperties dynamicThreadPoolProperties) {
        ConfigService configService = nacosConfigProperties.configServiceInstance();
        try {
            configService.addListener(dynamicThreadPoolProperties.getNacosDataId(), dynamicThreadPoolProperties.getNacosGroup(), new AbstractListener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    new Thread(() -> refreshThreadPoolExecutor()).start();
                    log.info("線程池配置有變化,刷新完成");
                }
            });
        } catch (NacosException e) {
            log.error("Nacos配置監聽異常", e);
        }
    }
    

    然後再刷新線程池的參數信息,由於監聽事件觸發的時候,這個時候配置其實還沒刷新,所以我就等待了 1 秒鐘,讓配置完成刷新然後直接從配置類取值。

    雖然有點挫還是可以用,其實更好的方式是解析 receiveConfigInfo 那個 configInfo,configInfo 就是改變之後的整個配置內容。因為不太好解析成屬性文件,就沒做,後面再改吧。

    /**
     * 刷新線程池
     */
    private void refreshThreadPoolExecutor() {
        try {
            // 等待配置刷新完成
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        dynamicThreadPoolProperties.getExecutors().forEach(executor -> {
            ThreadPoolExecutor threadPoolExecutor = threadPoolExecutorMap.get(executor.getThreadPoolName());
            threadPoolExecutor.setCorePoolSize(executor.getCorePoolSize());
            threadPoolExecutor.setMaximumPoolSize(executor.getMaximumPoolSize());
            threadPoolExecutor.setKeepAliveTime(executor.getKeepAliveTime(), executor.getUnit());
            threadPoolExecutor.setRejectedExecutionHandler(getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()));
            BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
            if (queue instanceof ResizableCapacityLinkedBlockIngQueue) {
                ((ResizableCapacityLinkedBlockIngQueue<Runnable>) queue).setCapacity(executor.getQueueCapacity());
            }
        });
    }
    

    其他的刷新都是線程池自帶的,需要注意的是線程池隊列大小的刷新,目前只支持 LinkedBlockingQueue 隊列,由於 LinkedBlockingQueue 的大小是不允許修改的,所以按照美團那篇文章提供的思路,自定義了一個可以修改的隊列,其實就是把 LinkedBlockingQueue 的代碼複製了一份,改一下就可以。

    往 Cat 上報運行信息

    往 Cat 的 Heartbeat 報表上傳數據的代碼如下,主要還是 Cat 本身提供了擴展的能力。只需要定時去調用下面的方式上報數據即可。

    public void registerStatusExtension(ThreadPoolProperties prop, KittyThreadPoolExecutor executor) {
        StatusExtensionRegister.getInstance().register(new StatusExtension() {
            @Override
            public String getId() {
                return "thread.pool.info." + prop.getThreadPoolName();
            }
            @Override
            public String getDescription() {
                return "線程池監控";
            }
            @Override
            public Map<String, String> getProperties() {
                AtomicLong rejectCount = getRejectCount(prop.getThreadPoolName());
                Map<String, String> pool = new HashMap<>();
                pool.put("activeCount", String.valueOf(executor.getActiveCount()));
                pool.put("completedTaskCount", String.valueOf(executor.getCompletedTaskCount()));
                pool.put("largestPoolSize", String.valueOf(executor.getLargestPoolSize()));
                pool.put("taskCount", String.valueOf(executor.getTaskCount()));
                pool.put("rejectCount", String.valueOf(rejectCount == null ? 0 : rejectCount.get()));
                pool.put("waitTaskCount", String.valueOf(executor.getQueue().size()));
                return pool;
            }
        });
    }
    

    定義線程池端點

    通過自定義端點來暴露線程池的配置和運行的情況,可以讓外部的監控系統拉取數據做對應的處理。

    @Endpoint(id = "thread-pool")
    public class ThreadPoolEndpoint {
        @Autowired
        private DynamicThreadPoolManager dynamicThreadPoolManager;
        @Autowired
        private DynamicThreadPoolProperties dynamicThreadPoolProperties;
        @ReadOperation
        public Map<String, Object> threadPools() {
            Map<String, Object> data = new HashMap<>();
            List<Map> threadPools = new ArrayList<>();
            dynamicThreadPoolProperties.getExecutors().forEach(prop -> {
                KittyThreadPoolExecutor executor = dynamicThreadPoolManager.getThreadPoolExecutor(prop.getThreadPoolName());
                AtomicLong rejectCount = dynamicThreadPoolManager.getRejectCount(prop.getThreadPoolName());
                Map<String, Object> pool = new HashMap<>();
                Map config = JSONObject.parseObject(JSONObject.toJSONString(prop), Map.class);
                pool.putAll(config);
                pool.put("activeCount", executor.getActiveCount());
                pool.put("completedTaskCount", executor.getCompletedTaskCount());
                pool.put("largestPoolSize", executor.getLargestPoolSize());
                pool.put("taskCount", executor.getTaskCount());
                pool.put("rejectCount", rejectCount == null ? 0 : rejectCount.get());
                pool.put("waitTaskCount", executor.getQueue().size());
                threadPools.add(pool);
            });
            data.put("threadPools", threadPools);
            return data;
        }
    }
    

    Cat 監控線程池中線程的執行時間

    本來是將監控放在 KittyThreadPoolExecutor 的 execute,submit 方法里的。後面測試下來發現有問題,數據在 Cat 上確實有了,但是執行時間都是 1 毫秒,也就是沒生效。

    不說想必大家也知道,因為線程是後面單獨去執行的,所以再添加任務的地方埋點沒任務意義。

    後面還是想到了一個辦法來實現埋點的功能,就是利用線程池提供的 beforeExecute 和 afterExecute 兩個方法,在線程執行之前和執行之後都會觸發這兩個方法。

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        String threadName = Thread.currentThread().getName();
        Transaction transaction = Cat.newTransaction(threadPoolName, runnableNameMap.get(r.getClass().getSimpleName()));
        transactionMap.put(threadName, transaction);
        super.beforeExecute(t, r);
    }
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        String threadName = Thread.currentThread().getName();
        Transaction transaction = transactionMap.get(threadName);
        transaction.setStatus(Message.SUCCESS);
        if (t != null) {
            Cat.logError(t);
            transaction.setStatus(t);
        }
        transaction.complete();
        transactionMap.remove(threadName);
    }
    

    後面的代碼大家自己去看就行了,本文到這裏就結束了。如果感覺本文還不錯的記得轉發下哦!

    多謝多謝。

    最後感謝美團技術團隊的那篇文章,雖然沒有分享源碼,但是思路什麼的和應用場景都講的很明白。

    感興趣的 Star 下唄:https://github.com/yinjihuan/kitty

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※推薦評價好的iphone維修中心

  • MySQL多版本併發控制機制(MVCC)-源碼淺析

    MySQL多版本併發控制機制(MVCC)-源碼淺析

    MySQL多版本併發控制機制(MVCC)-源碼淺析

    前言

    作為一個數據庫愛好者,自己動手寫過簡單的SQL解析器以及存儲引擎,但感覺還是不夠過癮。<<事務處理-概念與技術>>誠然講的非常透徹,但只能提綱挈領,不能讓你玩轉某個真正的數據庫。感謝cmake,能夠讓我在mac上用xcode去debug MySQL,從而能去領略它的各種實現細節。
    筆者一直對數據庫的隔離性很好奇,此篇博客就是我debug MySQL過程中的偶有所得。
    (注:本文的MySQL採用的是MySQL-5.6.35版本)

    MVCC(多版本併發控制機制)

    隔離性也可以被稱作併發控制、可串行化等。談到併發控制首先想到的就是鎖,MySQL通過使用兩階段鎖的方式實現了更新的可串行化,同時為了加速查詢性能,採用了MVCC(Multi Version Concurrency Control)的機制,使得不用鎖也可以獲取一致性的版本。

    Repeatable Read

    MySQL的通過MVCC以及(Next-Key Lock)實現了可重複讀(Repeatable Read),其思想(MVCC)就是記錄數據的版本變遷,通過精巧的選擇不同數據的版本從而能夠對用戶呈現一致的結果。如下圖所示:

    上圖中,(A=50|B=50)的初始版本為1。
    1.事務t1在select A時候看到的版本為1,即A=50
    2.事務t2對A和B的修改將版本升級為2,即A=0,B=100
    3.事務t1再此select B的時候看到的版本還是1, 即B=50
    這樣就隔離了版本的影響,A+B始終為100。

    Read Commit

    而如果不通過版本控制機制,而是讀到最近提交的結果的話,則隔離級別是read commit,如下圖所示:

    在這種情況下,就需要使用鎖機制(例如select for update)將此A,B記錄鎖住,從而獲得正確的一致結果,如下圖所示:

    MVCC的優勢

    當我們要對一些數據做一些只讀操作來檢查一致性,例如檢查賬務是否對齊的操作時候,並不希望加上對性能損耗很大的鎖。這時候MVCC的一致性版本就有很大的優勢了。

    MVCC(實現機制)

    本節就開始談談MVCC的實現機制,注意MVCC僅僅在純select時有效(不包括select for update,lock in share mode等加鎖操作,以及update\insert等)。

    select運行棧

    首先我們追蹤一下一條普通的查詢sql在mysql源碼中的運行過程,sql為(select * from test);

    其運行棧為:

    handle_one_connection  MySQL的網絡模型是one request one thread
     |-do_handle_one_connection
    	|-do_command
    		|-dispatch_command
    			|-mysql_parse	解析SQL
    				|-mysql_execute_command
    					|-execute_sqlcom_select	執行select語句
    						|-handle_select
    							...一堆parse join 等的操作,當前並不關心
    							|-*tab->read_record.read_record 讀取記錄
    

    由於mysql默認隔離級別是repeatable_read(RR),所以read_record重載為
    rr_sequential(當前我們並不關心select通過index掃描出row之後再通過condition過濾的過程)。繼續追蹤:

    read_record
     |-rr_sequential
    	|-ha_rnd_next
    		|-ha_innobase::rnd_next 這邊就已經到了innodb引擎了
    			|-general_fetch
    				|-row_search_for_mysql
    					|-lock_clust_rec_cons_read_sees 這邊就是判斷並選擇版本的地方
    

    讓我們看下該函數內部:

    bool lock_clust_rec_cons_read_sees(const rec_t* rec /*由innodb掃描出來的一行*/,....){
    	...
    	// 從當前掃描的行中獲取其最後修改的版本trx_id(事務id)
    	trx_id = row_get_rec_trx_id(rec, index, offsets);
    	// 通過參數(一致性快照視圖和事務id)決定看到的行快照
    	return(read_view_sees_trx_id(view, trx_id));
    }
    

    read_view的創建過程

    我們先關注一致性視圖的創建過程,我們先看下read_view結構:

    struct read_view_t{
    	// 由於是逆序排列,所以low/up有所顛倒
    	// 能看到當前行版本的高水位標識,>= low_limit_id皆不能看見
    	trx_id_t	low_limit_id;
    	// 能看到當前行版本的低水位標識,< up_limit_id皆能看見
    	trx_id_t	up_limit_id;
    	// 當前活躍事務(即未提交的事務)的數量
    	ulint		n_trx_ids;
    	// 以逆序排列的當前獲取活躍事務id的數組
    	// 其up_limit_id<tx_id<low_limit_id
    	trx_id_t*	trx_ids;	
    	// 創建當前視圖的事務id
    	trx_id_t	creator_trx_id;
    	// 事務系統中的一致性視圖鏈表
    	UT_LIST_NODE_T(read_view_t) view_list;
    };
    

    然後通過debug,發現創建read_view結構也是在上述的rr_sequential中操作的,繼續跟蹤調用棧:

    rr_sequential
     |-ha_rnd_next
     	|-rnd_next
     		|-index_first 在start_of_scan為true時候走當前分支index_first
     			|-index_read
     				|-row_search_for_mysql
     					|-trx_assign_read_view
    

    我們看下row_search_for_mysql里的一個分支:

    row_search_for_mysql:
    // 這邊只有select不加鎖模式的時候才會創建一致性視圖
    else if (prebuilt->select_lock_type == LOCK_NONE) {		// 創建一致性視圖
    		trx_assign_read_view(trx);
    		prebuilt->sql_stat_start = FALSE;
    }
    

    上面的註釋就是select for update(in share model)不會走MVCC的原因。讓我們進一步分析trx_assign_read_view函數:

    trx_assign_read_view
     |-read_view_open_now
     	|-read_view_open_now_low
    

    好了,終於到了創建read_view的主要階段,主要過程如下圖所示:

    代碼過程為:

    static read_view_t* read_view_open_now_low(trx_id_t	cr_trx_id,mem_heap_t*	heap)
    {
    	read_view_t*	view;
    	// 當前事務系統中max_trx_id(即尚未被分配的trx_id)設置為low_limit_no
    	view->low_limit_no = trx_sys->max_trx_id;
    	view->low_limit_id = view->low_limit_no;
    	// CreateView構造函數,會將非當前事務和已經在內存中提交的事務給剔除,即判斷條件為
    	// trx->id != m_view->creator_trx_id&& !trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY)的
    	// 才加入當前視圖列表
    	ut_list_map(trx_sys->rw_trx_list, &trx_t::trx_list, CreateView(view));
    	if (view->n_trx_ids > 0) {
    		// 將當前事務系統中的最小id設置為up_limit_id,因為是逆序排列
    		view->up_limit_id = view->trx_ids[view->n_trx_ids - 1];
    	} else {
    		// 如果當前沒有非當前事務之外的活躍事務,則設置為low_limit_id
    		view->up_limit_id = view->low_limit_id;
    	}
    	// 忽略purge事務,purge時,當前事務id是0
    	if (cr_trx_id > 0) {
    		read_view_add(view);
    	}
    	// 返回一致性視圖
    	return(view);
    }
    

    行版本可見性:

    由上面的lock_clust_rec_cons_read_sees可知,行版本可見性由read_view_sees_trx_id函數判斷:

    /*********************************************************************//**
    Checks if a read view sees the specified transaction.
    @return	true if sees */
    UNIV_INLINE
    bool
    read_view_sees_trx_id(
    /*==================*/
    	const read_view_t*	view,	/*!< in: read view */
    	trx_id_t		trx_id)	/*!< in: trx id */
    {
    	if (trx_id < view->up_limit_id) {
    
    		return(true);
    	} else if (trx_id >= view->low_limit_id) {
    
    		return(false);
    	} else {
    		ulint	lower = 0;
    		ulint	upper = view->n_trx_ids - 1;
    
    		ut_a(view->n_trx_ids > 0);
    
    		do {
    			ulint		mid	= (lower + upper) >> 1;
    			trx_id_t	mid_id	= view->trx_ids[mid];
    
    			if (mid_id == trx_id) {
    				return(FALSE);
    			} else if (mid_id < trx_id) {
    				if (mid > 0) {
    					upper = mid - 1;
    				} else {
    					break;
    				}
    			} else {
    				lower = mid + 1;
    			}
    		} while (lower <= upper);
    	}
    
    	return(true);
    }
    

    其實上述函數就是一個二分法,read_view其實保存的是當前活躍事務的所有事務id,如果當前行版本對應修改的事務id不在當前活躍事務裏面的話,就返回true,表示當前版本可見,否則就是不可見,如下圖所示。

    接上述lock_clust_rec_cons_read_sees的返回:

    if (UNIV_LIKELY(srv_force_recovery < 5)
    			    && !lock_clust_rec_cons_read_sees(
    				    rec, index, offsets, trx->read_view)){
    	// 當前處理的是當前版本不可見的情況
    	// 通過undolog來返回到一致的可見版本
    	err = row_sel_build_prev_vers_for_mysql(
    					trx->read_view, clust_index,
    					prebuilt, rec, &offsets, &heap,
    					&old_vers, &mtr);			    
    } else{
    	// 可見,然後返回
    }
    

    undolog搜索可見版本的過程

    我們現在考察一下row_sel_build_prev_vers_for_mysql函數:

    row_sel_build_prev_vers_for_mysql
     |-row_vers_build_for_consistent_read
    

    主要是調用了row_ver_build_for_consistent_read方法返回可見版本:

    dberr_t row_vers_build_for_consistent_read(...)
    {
    	......
    	for(;;){
    		err = trx_undo_prev_version_build(rec, mtr,version,index,*offsets, heap,&prev_version);
    		......
    		trx_id = row_get_rec_trx_id(prev_version, index, *offsets);
    		// 如果當前row版本符合一致性視圖,則返回
    		if (read_view_sees_trx_id(view, trx_id)) {
    			......
    			break;
    		}
    		// 如果當前row版本不符合,則繼續回溯上一個版本(回到for循環的地方)
    		version = prev_version;
    	}
    	......
    }
    

    整個過程如下圖所示:

    至於undolog怎麼恢復出對應版本的row記錄就又是一個複雜的過程了,由於篇幅原因,在此略過不表。

    read_view創建時機再討論

    在創建一致性視圖的row_search_for_mysql的代碼中

    // 只有非鎖模式的select才創建一致性視圖
    else if (prebuilt->select_lock_type == LOCK_NONE) {		// 創建一致性視圖
    		trx_assign_read_view(trx);
    		prebuilt->sql_stat_start = FALSE;
    }
    

    trx_assign_read_view中由這麼一段代碼

    // 一致性視圖在一個事務只創建一次
    if (!trx->read_view) {
    		trx->read_view = read_view_open_now(
    			trx->id, trx->global_read_view_heap);
    		trx->global_read_view = trx->read_view;
    	}
    
    

    所以綜合這兩段代碼,即在一個事務中,只有第一次運行select(不加鎖)的時候才會創建一致性視圖,如下圖所示:

    筆者構造了此種場景模擬過,確實如此。

    MVCC和鎖的同時作用導致的一些現象

    MySQL是通過MVCC和二階段鎖(2PL)來兼顧性能和一致性的,但是由於MySQL僅僅在select時候才創建一致性視圖,而在update等加鎖操作的時候並不做如此操作,所以就會產生一些詭異的現象。如下圖所示:

    如果理解了update不走一致性視圖(read_view),而select走一致性視圖(read_view),就可以很好解釋這個現象。
    如下圖所示:

    總結

    MySQL為了兼顧性能和ACID使用了大量複雜的機制,2PL(兩階段鎖)和MVCC就是其實現的典型。幸好可以通過xcode等IDE進行方便的debug,這樣就可以非常精確加便捷的追蹤其各種機制的實現。希望這篇文章能夠幫助到喜歡研究MySQL源碼的讀者們。

    公眾號

    關注筆者公眾號,獲取更多乾貨文章:

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※回頭車貨運收費標準

  • JSR133提案-修復Java內存模型

    目錄

    • 1. 什麼是內存模型?
    • 2. JSR 133是關於什麼的?
    • 3. 再談指令重排序
    • 4.同步都做了什麼?
    • 5. final字段在舊的內存模型中為什麼可以改變?
    • 6.“初始化安全”與final字段?
    • 7. 增強volatile語義
    • 8. 修復“double-checked locking”的問題
    • 9. 為什麼要關心這些問題?
    • 延伸閱讀

    1. 什麼是內存模型?

    在多處理器系統中,為了提高訪問數據的速度,通常會增加一層或多層高速緩存(越靠近處理器的緩存速度越快)。
    但是緩存同時也帶來了許多新的挑戰。比如,當兩個處理器同時讀取同一個內存位置時,看到的結果可能會不一樣?
    在處理器維度上,內存模型定義了一些規則來保證當前處理器可以立即看到其他處理器的寫入,以及當前處理器的寫入對其他處理器立即可見。這些規則被稱為緩存一致性協議
    有些多處理器架構實現了強一致性,所有的處理器在同一時刻看到的同一內存位置的值是一樣的。
    而其他處理器實現的則是較弱的一致性,需要使用被稱為內存屏障的特殊機器指令使來實現最終一致性(通過刷新緩存或使緩存失效)。
    這些內存屏障通常在釋放鎖和獲取鎖時被執行;對於高級語言(如Java)的程序員來說,它們是不可見的。

    在強一致性的處理器上,由於減少了對內存屏障的依賴,編寫併發程序會更容易一些。
    但是,相反的,近年來處理器設計的趨勢是使用較弱的內存模型,因為放寬對緩存一致性的要求可以使得多處理器系統有更好的伸縮性和更大的內存。

    此外,編譯器、緩存或運行時還被允許通過指令重排序改變內存的操作順序(相對於程序所表現的順序)。
    例如,編譯器可能會往後移動一個寫入操作,只要移動操作不改變程序的原本語義(as-if-serial語義),就可以自由進行更改。
    再比如,緩存可能會推遲把數據刷回到主內存中,直到它認為時機合適了。
    這種靈活的設計,目的都是為了獲得得最佳的性能,
    但是在多線程環境下,指令重排會使得跨線程可見性的問題變的更複雜。

    為了方便理解,我們來看個代碼示例:

    Class Reordering {
      int x = 0, y = 0;
      //thread A
       public void writer() {
            x = 1;
            y = 2;
        }
    
        //thread B
        public void reader() {
            int r1 = y;
            int r2 = x;
         }
    }
    

    假設這段代碼被兩個線程併發執行,線程A執行writer(),線程B執行reader()。
    如果線程B在reader()中看到了y=2,那麼直覺上我們會認為它看到的x肯定是1,因為在writer()中x=1y=2之前 。
    然而,發生重排序時y=2會早於x=1執行,此時,實際的執行順序會是這樣的:

    y=2;
    int r1=y;
    int r2=x;
    x=1;
    

    結果就是,r1的值是2,r2的值是0。
    從線程A的角度看,x=1與y=2哪個先執行結果是一樣的(或者說沒有違反as-if-serial語義),但是在多線程環境下,這種重排序會產生混亂的結果。

    我們可以看到,高速緩存指令重排序提高了效率的同時也引出了新的問題,這顯然使得編寫併發程序變得更加困難。
    Java內存模型就是為了解決這類問題,它對多線程之間如何通過內存進行交互做了明確的說明。
    更具體點,Java內存模型描述了程序中的變量與實際計算機的存儲設備(包括內存、緩存、寄存器)之間交互的底層細節。
    例如,Java提供了volatile、final和 synchronized等工具,用於幫助程序員向編譯器表明對併發程序的要求。
    更重要的是,Java內存模型保證這些同步工具可以正確的運行在任何處理器架構上,使Java併發應用做到“Write Once, Run Anywhere”。

    相比之下,大多數其他語言(例如C/C++)都沒有提供显示的內存模型。
    C程序繼承了處理器的內存模型,這意味着,C語言的併發程序在一個處理器架構中可以正確運行,在另外一個架構中則不一定。

    2. JSR 133是關於什麼的?

    Java提供的跨平台內存模型是一個雄心勃勃的計劃,在當時是具有開創性的。
    但不幸的是,定義一個即直觀又一致的內存模型比預期的要困難得多。
    自1997年以來,在《Java語言規範》的第17章關於Java內存模型的定義中發現了一些嚴重的缺陷。
    這些缺陷使一些同步工具產生混亂的結果,例如final字段可以被更改。
    JSR 133為Java語言定義了一個新的內存模型,修復了舊版內存模型的缺陷(修改了final和volatile的語義)
    JSR的主要目標包括不限於這些:

    1. 正確同步的語義應該更直觀更簡單。
    2. 應該定義不完整或不正確同步的語義,以最小化潛在的安全隱患
    3. 程序員應該有足夠的自信推斷出多線程程序如何與內存交互的。
    4. 提供一個新的初始化安全性保證(initialization safety)。
      如果一個對象被正確初始化了(初始化期間,對象的引用沒有逃逸,比如構造函數里把this賦值給變量),那麼所有可以看到該對象引用的線程,都可以看到在構造函數中被賦值的final變量。這不需要使用synchronized或volatile。

    3. 再談指令重排序

    在許多情況下,出於優化執行效率的目的,數據(實例變量、靜態字段、數組元素等)可以在寄存器、緩存和內存之間以不同於程序中聲明的順序被移動。
    例如,線程先寫入字段a,再寫入字段b,並且b的值不依賴a,那麼編譯器就可以自由的對這些操作重新排序,在寫入a之前把b的寫入刷回到內存。
    除了編譯器,重排序還可能發生在JIT、緩存、處理器上。
    無論發生在哪裡,重排序都必須遵循as-if-serial語義,這意味着在單線程程序中,程序不會覺察到重排序的存在,或者說給單線程程序一種沒有發生過重排序的錯覺。
    但是,重排序在沒有同步的多線程程序中會產生影響。在這種程序中,一個線程能夠觀察到其他線程的運行情況,並且可能檢測到變量訪問順序與代碼中指定的順序不一致。
    大多數情況下,一個線程不會在乎另一個線程在做什麼,但是,如果有,就是同步的用武之地。

    4.同步都做了什麼?

    同步有很多面,最為程序員熟知的是它的互斥性,同一時刻只能有一個線程持有monitor。
    但是,同步不僅僅是互斥性。同步還能保證一個線程在同步塊中的寫內存操作對其他持有相同monitor的線程立即可見。
    當線程退出同步塊時(釋放monitor),會把緩存中的數據刷回到主內存,使主內存中保持最新的數據。
    當線程進入同步塊時(獲取monitor),會使本地處理器緩存失效,使得變量必須從主內存中重新加載。
    我們可以看到,之前的所有寫操作對後來的線程都是可見的。

    5. final字段在舊的內存模型中為什麼可以改變?

    證明final字段可以改變的最佳示例是String類的實現(JDK 1.4版本)。
    String對象包含三個字段:一個字符串數組的引用value、一個記錄數組中開始位置的offset、字符串長度length。
    通過這種方式,可以實現多個String/StringBuffer對象共享一個相同的字符串數組,從而避免為每個對象分配額外的空間。
    例如,String.substring()通過與原String對象共享一個數組來產生一個新的對象,唯一的不同是length和offset字段。

    String s1 = "/usr/tmp";
    String s2 = s1.substring(4); 
    

    s2和s1共享一個字符串數組”/usr/tmp”,不同的是s2的offset=4,length=4,s1的offset=0,length=8。
    在String的構造函數運行之前,根類Object的構造函數會先初始化所有字段為默認值,包括final的length和offset字段。
    當String的構造函數運行時,再把length和offset賦值為期望的值。
    但是這一過程,在舊的內存模型中,如果沒有使用同步,另一個線程可能會看到offset的默認值0,然後在看到正確的值4.
    結果導致一個迷幻的現象,開始看到字符串s2的內容是’/usr’,然後再看到’/tmp’。
    這不符合我們對final語義的認識,但是在舊內存模型中確實存在這樣的問題。
    (JDK7開始,改變了substring的實現方式,每次都會創建一個新的對象)

    6.“初始化安全”與final字段?

    新的內存模型提供一個新初始化安全( initialization safety)保障。
    意味着,只要一個對象被正確的構造,那麼所有的線程都會看到這些在構造函數中被賦值的final字段。
    “正確”的構造是指在構造函數執行期間,對象的引用沒有發生逃逸。或者說,在構造函數中沒有把該對象的引用賦值給任何變量。

    class FinalFieldExample {
      final int x;
      int y;
      static FinalFieldExample f;
      public FinalFieldExample() {
        x = 3;
        y = 4;
      }
    
      static void writer() {
        f = new FinalFieldExample();
      }
    
      static void reader() {
        if (f != null) {
          int i = f.x;
          int j = f.y;
        }
      }
    }
    

    示例中,初始化安全保證執行reader()方法的線程看到的f.x=3,因為它是final字段,但是不保證能看到y=4,因為它不是final的。
    但是如果構造函數像這樣:

    public FinalFieldExample() { // bad!
      x = 3;
      y = 4;
      global.obj = this;  //  allowing this to escape
    }
    

    初始化安全不能保證讀取global.obj的線程看到的x的值是3,因為對象引用this發生了逃逸。

    不僅如此,任何通過final字段(構造函數中被賦值的)可以觸達的變量都可以保證對其他線程可見。
    這意味着如果一個final字段包含一個引用,例如ArrayList,除了該字段的引用對其他線程可見,ArrayList中的元素對其他線程也是可見的。

    初始化安全增強了final的語義,使其更符合我們對final的直觀感受,任何情況下都不會改變。

    7. 增強volatile語義

    volatile變量是用於線程之間傳遞狀態的特殊變量,這要求任何線程看到的都是volatile變量的最新值。
    為實現可見性,禁止在寄存器中分配它們,還必須確保修改volatile后,要把最新值從緩存刷到內存中。
    類似的,在讀取volatile變量之前,必須使高速緩存失效,這樣其他線程會直接讀取主內存中的數據。
    在舊的內存模型中,多個volatile變量之間不能互相重排序,但是它們被允許可以與非volatile變量一起重排序,這消弱了volatile作為線程間交流信號的作用。
    我們來看個示例:

    Map configs;
    volatile boolean initialized = false;
    . . .
     
    // In thread A
    configs  =  readConfigFile(fileName);
    processConfigOptions( configs);
    initialized = true;
    . . .
     
    // In thread B
    while (initialized) {
        // use configs
    }
    

    示例中,線程A負責配置數據初始化工作,初始化完成后線程B開始執行。
    實際上,volatile變量initialized扮演者守衛者的角色,它表示前置工作已經完成,依賴這些數據的其他線程可以執行了。
    但是,當volatile變量與非volatile變量被編譯器放到一起重新排序時,“守衛者”就形同虛設了。
    重排序發生時,可能會使readConfigFile()中某個動作在initialized = true之後執行,
    那麼,線程B在看到initialized的值為true后,在使用configs對象時,會讀取到沒有被正確初始化的數據。
    這是volatile很典型的應用場景,但是在舊的內存模型中卻不能正確的工作。

    JSR 133專家組決定在新的內存模型中,不再允許volatile變量與其他任務內存操作一起重排序
    這意味着,volatile變量之前的內存操作不會在其後執行,volatile變量之後的內存操作不會在其前執行。
    volatile變量相當於一個屏障,重排序不能越過對volatile的內存操作。(實際上,jvm確實使用了內存屏障指令)
    增強volatile語義的副作用也很明顯,禁止重排序會有一定的性能損失。

    8. 修復“double-checked locking”的問題

    double-checked locking是單例模式的其中一種實現,它支持懶加載且是線程安全的。
    大概長這個樣子:

    private static Something instance = null;
    
    public Something getInstance() {
      if (instance == null) {
        synchronized (this) {
          if (instance == null)
            instance = new Something();//
        }
      }
      return instance;
    }
    

    它通過兩次檢查巧妙的避開了在公共代碼路徑上使用同步,從而避免了同步所帶來的性能開銷。
    它唯一的問題就是——不起作用。為什麼呢?
    instance的賦值操作會與SomeThing()構造函數中的變量初始化一起被編譯器或緩存重排序,這可能會導致把未完全初始化的對象引用賦值給instance。
    現在很多人知道把instance聲明為volatile可以修復這個問題,但是在舊的內存模型(JDK 1.5之前)中並不可行,原因前面有提到,volatile可以與非volatile字段一起重排序。

    儘管,新的內存模型修復了double-checked locking的問題,但仍不鼓勵這種實現方式,因為volatile並不是免費的。
    相比之下,Initialization On Demand Holder Class更值得被推薦,
    它不僅實現了懶加載和線程安全,還提供了更好的性能和更清晰的代碼邏輯。大概長這個樣子:

    public class Something {
        private Something() {}
        //static innner class
        private static class LazyHolder {
            static final Something INSTANCE = new Something(); //static  field
        }
    
        public static Something getInstance() {
            return LazyHolder.INSTANCE;
        }
    }
    

    這種實現完全沒有使用同步工具,而是利用了Java語言規範的兩個基本原則,
    其一,JVM保證靜態變量的初始化對所有使用該類的線程立即可見;
    其二,內部類首次被使用時才會觸發類的初始化,這實現了懶加載。

    9. 為什麼要關心這些問題?

    併發問題一般不會在測試環境出現,生成環境的併發問題又不容易復現,這兩個特點使得併發問題通常比較棘手。
    所以你最好提前花點時間學習併發知識,以確保寫出正確的併發程序。我知道這很困難,但是應該比排查生產環境的併發問題容易的多。

    延伸閱讀

    1.JSR 133 (Java Memory Model) FAQ,2004
    2.volatile關鍵字
    3.Double-checked問題
    4.內存屏障和volatile語義
    5.修復Java內存模型
    6.String substring 在jdk7中會創建新的數組
    7.Memory Ordering
    8.有MESI協議為什麼還需要volatile?
    9.Initialization On Demand Holder Class
    10.The JSR-133 Cookbook for Compiler Writers

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • TCP 重置攻擊的工作原理

    TCP 重置攻擊的工作原理

    原文鏈接:https://fuckcloudnative.io/posts/deploy-k3s-cross-public-cloud/

    TCP 重置攻擊 是使用一個單一的數據包來執行的,只有幾個字節大小。攻擊者製作併發送一個偽造的 TCP 重置包來干擾用戶和網站的連接,欺騙通信雙方終止 TCP 連接。我們偉大的 xx 長城便運用了這個技術來進行 TCP 關鍵字阻斷。

    理解 TCP 重置攻擊並不需要具備深厚的網絡知識功底,只需要一台筆記本就可以對自己進行模擬攻擊。本文將會帶你了解 TCP 重置攻擊的原理,同時會幫助你理解很多關於 TCP 協議的特性。本文主要內容:

    • 回顧 TCP 協議的基礎知識
    • 了解 TCP 重置攻擊的原理
    • 使用一個簡單的 Python 腳本來模擬攻擊

    下面開始分析 TCP 重置攻擊原理。

    1. 偉大的 xx 長城是如何利用 TCP 重置攻擊的?

    這一段略過,原因你懂得,感興趣的請直接看原文。

    2. TCP 重置攻擊的工作原理

    在 TCP 重置攻擊中,攻擊者通過向通信的一方或雙方發送偽造的消息,告訴它們立即斷開連接,從而使通信雙方連接中斷。正常情況下,如果客戶端收發現到達的報文段對於相關連接而言是不正確的,TCP 就會發送一個重置報文段,從而導致 TCP 連接的快速拆卸。

    TCP 重置攻擊利用這一機制,通過向通信方發送偽造的重置報文段,欺騙通信雙方提前關閉 TCP 連接。如果偽造的重置報文段完全逼真,接收者就會認為它有效,並關閉 TCP 連接,防止連接被用來進一步交換信息。服務端可以創建一個新的 TCP 連接來恢復通信,但仍然可能會被攻擊者重置連接。萬幸的是,攻擊者需要一定的時間來組裝和發送偽造的報文,所以一般情況下這種攻擊只對長連接有殺傷力,對於短連接而言,你還沒攻擊呢,人家已經完成了信息交換。

    從某種意義上來說,偽造 TCP 報文段是很容易的,因為 TCP/IP 都沒有任何內置的方法來驗證服務端的身份。有些特殊的 IP 擴展協議(例如 IPSec)確實可以驗證身份,但並沒有被廣泛使用。客戶端只能接收報文段,並在可能的情況下使用更高級別的協議(如 TLS)來驗證服務端的身份。但這個方法對 TCP 重置包並不適用,因為 TCP 重置包是 TCP 協議本身的一部分,無法使用更高級別的協議進行驗證。

    儘管偽造 TCP 報文段很容易,但偽造正確的 TCP 重置報文段並完成攻擊卻並不容易。為了理解這項工作的難度,我們需要先了解一下 TCP 協議的工作原理。

    3. TCP 協議工作原理

    TCP 協議的目標是向客戶端發送一份完整的數據副本。例如,如果我的服務器通過 TCP 連接向你的計算機發送我的網站的 HTML,你的計算機的 TCP 協議棧應該能夠以我發送的形式和順序輸出 HTML

    然而現實生活中我的 HTML 內容並不是按順序發送的,它被分解成許多小塊(稱為 TCP 分組),每個小塊在網絡上被單獨發送,並被重新組合成原來發送的順序。這種重新組合后的輸出被稱為 TCP 字節流

    將分組重建成字節流並不簡單,因為網絡是不可靠的。TCP分組可能會被丟棄,可能不按發送的順序到達客戶端,也可能會被重複發送、報文損壞等等。因此,TCP 協議的職責是在不可靠的網絡上提供可靠的通信。TCP 通過要求連接雙方保持密切聯繫,持續報告它們接收到了哪些數據來實現可靠通信,這樣服務端就能夠推斷出客戶端尚未接收到的數據,並重新發送丟失的數據。

    為了進一步理解這個過程,我們需要了解服務端和客戶端是如何使用序列號(sequence numbers)來標記和跟蹤數據的。

    TCP 序列號

    TCP 協議的通信雙方, 都必須維護一個序列號(sequence numbers),對於客戶端來說,它會使用服務端的序列號來將接收到的數據按照發送的順序排列。

    當通信雙方建立 TCP 連接時,客戶端與服務端都會向對方發送一個隨機的初始序列號,這個序列號標識了其發送數據流的第一個字節。TCP 報文段包含了 TCP 頭部,它是附加在報文段開頭的元數據,序列號就包含在 TCP 頭部中。由於 TCP 連接是雙向的,雙方都可以發送數據,所以 TCP 連接的雙方既是發送方也是接收方,每一方都必須分配和管理自己的序列號。

    確認應答

    當接收方收到一個 TCP 報文段時,它會向發送方返回一個 ACK 應答報文(同時將 TCP 頭部的 ACK 標誌位置 1),這個 ACK 號就表示接收方期望從發送方收到的下一個字節的序列號。發送方利用這個信息來推斷接收方已經成功接收到了序列號為 ACK 之前的所有字節。

    TCP 頭部格式如下圖所示:

    一個確認應答報文的 TCP 頭部必須包含兩個部分:

    • ACK 標誌位置位 1
    • 包含確認應答號(ACK number)

    TCP 總共有 6 個標誌位,下文就會講到其中的 RST 標誌位。

    TCP 頭部包含了多個選項,其中有一個選擇確認選項(SACK),如果使用該選項,那麼當接收方收到了某個範圍內的字節而不是連續的字節時,就會發送 SACK 告知對方。例如,只收到了字節 1000~30004000~5000,但沒有收到 3001~3999。為了簡單起見,下文討論 TCP 重置攻擊時將忽略選擇確認選項。

    如果發送方發送了報文後在一段時間內沒有收到 ACK,就認為報文丟失了,並重新發送報文,用相同的序列號標記。這就意味着,如果接收方收到了重複的報文,可以使用序列號來判斷是否見過這個報文,如果見過則直接丟棄。網絡環境是錯綜複雜的,往往並不是如我們期望的一樣,先發送的數據包,就先到達目標主機,反而它很騷,可能會由於網絡擁堵等亂七八糟的原因,會使得舊的數據包,先到達目標主機。一般分兩種情況:

    1. 發送的數據包丟失了
    2. 發送的數據包被成功接收,但返回的 ACK 丟失了

    這兩種情況對發送方來說其實是一樣的,發送方並不能區分是哪種情況,所以只能重新發送數據包。

    只要不頻繁重複發送數據,額外的開銷基本可以忽略。

    為偽造的重置包選擇序列號

    構建偽造的重置包時需要選擇一個序列號。接收方可以接收序列號不按順序排列的報文段,但這種容忍是有限度的,如果報文段的序列號與它期望的相差甚遠,就會被直接丟棄。

    因此,一個成功的 TCP 重置攻擊需要構建一個可信的序列號。但什麼才是可信的序列號呢?對於大多數報文段(除了重置包,即 RST 包)來說,序列號是由接收方的接收窗口大小決定的。

    TCP 滑動窗口大小

    想象一下,將一台上世紀 90 年代初的古老計算機,連接到現代千兆光纖網絡。閃電般快速的網絡可以以令人瞠目結舌的速度向這台古老的計算機傳送數據,速度遠遠超過該計算機的處理能力。但並沒有什麼卵用,因為只有接收方接收並處理了報文,才能認為這個報文已經被收到了。

    TCP 協議棧有一個緩衝區,新到達的數據被放到緩衝區中等待處理。但緩衝區的大小是有限的,如果接收方的處理速度跟不上發送方的發送速度,緩衝區就會被填滿。一旦緩衝區被填滿,多餘的數據就會被直接丟棄,也不會返回 ACK。因此一旦接收方的緩衝區有了空位,發送方必須重新發送數據。也就是說,如果接收方的處理速度跟不上,發送方的發送速度再快也沒用。

    緩衝區到底有多大?發送方如何才能知道什麼時候可以一次發送更多的數據,什麼時候該一次發送很少的數據?這就要靠 TCP 滑動窗口了。接收方的滑動窗口大小是指發送方無需等待確認應答,可以持續發送數據的最大值。 假設接收方的通告窗口大小為 100,000 字節,那麼發送方可以無需等待確認應答,持續發送 100,000 個字節。再假設當發送方發送第 100,000 個字節時,接收方已經發送了前 10,000 個字節的 ACK,這就意味着窗口中還有 90,000 個字節未被確認,發送方還可以再持續發送 10,000 個字節。如果發送了 10,000 個字節的過程中沒有收到任何的 ACK,那麼接收方的滑動窗口將被填滿,發送方將停止發送新數據(可以繼續發送之前丟失的數據),直到收到相關的 ACK 才可以繼續發送。

    TCP 連接雙方會在建立連接的初始握手階段通告對方自己窗口的大小,後續還可以動態調整。TCP 緩衝區大的服務器可能會聲明一個大窗口,以便最大限度提高吞吐量。TCP 緩衝區小的服務器可能會被迫聲明一個小窗口,這樣做會犧牲一定的吞吐量,但為了防止接收方的 TCP 緩衝區溢出,還是很有必要的。

    換個角度來看,TCP 滑動窗口大小是對網絡中可能存在的未確認數據量的硬性限制。我們可以用它來計算髮送方在某一特定時間內可能發送的最大序列號(max_seq_no):

    max_seq_no = max_acked_seq_no + window_size
    

    其中 max_acked_seq_no 是接收方發送的最大 ACK 號,它表示發送方知道接收方已經成功接收的最大序列號。window_size 是窗口大小,它表示允許發送方最多發送的未被確認的字節。所以發送方可以發送的最大序列號是:max_acked_seq_no + window_size

    TCP 規範規定,接收方應該忽略任何序列號在接收窗口之外的數據。例如,如果接收方確認了所有序列號在 15,000 以下的字節,且接收窗口大小為 30,000,那麼接下來接收方只能接收序列號範圍在 15,000 ~ 45,000 之間的數據。如果一個報文段的部分數據在窗口內,另一部分數據在窗口外,那麼窗口內的數據將被接收確認,窗口外的數據將被丟棄。注意:這裏忽略了選擇確認選項,再強調一遍!

    對於大多數 TCP 報文段來說,滑動窗口的規則告訴了發送方自己可以接收的序列號範圍。但對於重置報文來說,序列號的限制更加嚴格,這是為了抵禦一種攻擊叫做盲目 TCP 重置攻擊(blind TCP reset attack),下文將會解釋。

    TCP 重置報文段的序列號

    對於 TCP 重置報文段來說,接收方對序列號的要求更加嚴格,只有當其序列號正好等於下一個預期的序列號時才能接收。繼續搬出上面的例子,接收方發送了一個確認應答,ACK 號為 15,000。如果接下來收到了一個重置報文,那麼其序列號必須是 15,000 才能被接收。

    如果重置報文的序列號超出了接收窗口範圍,接收方就會直接忽略該報文;如果其序列號在接收窗口範圍內,那麼接收方就會返回一個 challenge ACK,告訴發送方重置報文段的序列號是錯誤的,並告之正確的序列號,發送方可以利用 challenge ACK 中的信息來重新構建和發送重置報文。

    其實在 2010 年之前,TCP 重置報文段和其他報文段的序列號限制規則一樣,但無法抵禦盲目 TCP 重置攻擊,後來才採取這些措施施加額外的限制。

    盲目 TCP 重置攻擊

    如果攻擊者能夠截獲通信雙方正在交換的信息,攻擊者就能讀取其數據包上的序列號和確認應答號,並利用這些信息得出偽裝的 TCP 重置報文段的序列號。相反,如果無法截獲通信雙方的信息,就無法確定重置報文段的序列號,但仍然可以批量發出盡可能多不同序列號的重置報文,以期望猜對其中一個序列號。這就是所謂的盲目 TCP 重置攻擊(blind TCP reset attack)。

    在 2010 年之前 TCP 的原始版本中,攻擊者只需要猜對接收窗口內的隨便哪一個序列號即可,一般只需發送幾萬個報文段就能成功。採取額外限制的措施后,攻擊者需要發送數以百萬計的報文段才有可能猜對序列號,這幾乎是很難成功的。更多細節請參考 RFC-5963。

    4. 模擬攻擊

    以下實驗是在 OSX 系統中完成的,其他系統請自行測試。

    現在來總結一下偽造一個 TCP 重置報文要做哪些事情:

    • 嗅探通信雙方的交換信息。
    • 截獲一個 ACK 標誌位置位 1 的報文段,並讀取其 ACK 號。
    • 偽造一個 TCP 重置報文段(RST 標誌位置為 1),其序列號等於上面截獲的報文的 ACK 號。這隻是理想情況下的方案,假設信息交換的速度不是很快。大多數情況下為了增加成功率,可以連續發送序列號不同的重置報文。
    • 將偽造的重置報文發送給通信的一方或雙方,時其中斷連接。

    為了實驗簡單,我們可以使用本地計算機通過 localhost 與自己通信,然後對自己進行 TCP 重置攻擊。需要以下幾個步驟:

    1. 在兩個終端之間建立一個 TCP 連接。
    2. 編寫一個能嗅探通信雙方數據的攻擊程序。
    3. 修改攻擊程序,偽造併發送重置報文。

    下面正式開始實驗。

    建立 TCP 連接

    可以使用 netcat 工具來建立 TCP 連接,這個工很多操作系統都預裝了。打開第一個終端窗口,運行以下命令:

    $ nc -nvl 8000
    

    這個命令會啟動一個 TCP 服務,監聽端口為 8000。接着再打開第二個終端窗口,運行以下命令:

    $ nc 127.0.0.1 8000
    

    該命令會嘗試與上面的服務建立連接,在其中一個窗口輸入一些字符,就會通過 TCP 連接發送給另一個窗口並打印出來。

    嗅探流量

    編寫一個攻擊程序,使用 Python 網絡庫 scapy 來讀取兩個終端窗口之間交換的數據,並將其打印到終端上。完整的代碼參考我的 GitHub 倉庫,代碼的核心是調用 scapy 的嗅探方法:

    t = sniff(
            iface='lo0',
            lfilter=is_packet_tcp_client_to_server(localhost_ip, localhost_server_port, localhost_ip),
            prn=log_packet,
            count=50)
    

    這段代碼告訴 scapylo0 網絡接口上嗅探數據包,並記錄所有 TCP 連接的詳細信息。

    • iface : 告訴 scapy 在 lo0(localhost)網絡接口上進行監聽。
    • lfilter : 這是個過濾器,告訴 scapy 忽略所有不屬於指定的 TCP 連接(通信雙方皆為 localhost,且端口號為 8000)的數據包。
    • prn : scapy 通過這個函數來操作所有符合 lfilter 規則的數據包。上面的例子只是將數據包打印到終端,下文將會修改函數來偽造重置報文。
    • count : scapy 函數返回之前需要嗅探的數據包數量。

    發送偽造的重置報文

    下面開始修改程序,發送偽造的 TCP 重置報文來進行 TCP 重置攻擊。根據上面的解讀,只需要修改 prn 函數就行了,讓其檢查數據包,提取必要參數,並利用這些參數來偽造 TCP 重置報文併發送。

    例如,假設該程序截獲了一個從(src_ip, src_port)發往 (dst_ip, dst_port)的報文段,該報文段的 ACK 標誌位已置為 1,ACK 號為 100,000。攻擊程序接下來要做的是:

    • 由於偽造的數據包是對截獲的數據包的響應,所以偽造數據包的源 IP/Port 應該是截獲數據包的目的 IP/Port,反之亦然。
    • 將偽造數據包的 RST 標誌位置為 1,以表示這是一個重置報文。
    • 將偽造數據包的序列號設置為截獲數據包的 ACK 號,因為這是發送方期望收到的下一個序列號。
    • 調用 scapysend 方法,將偽造的數據包發送給截獲數據包的發送方。

    對於我的程序而言,只需將這一行取消註釋,並註釋這一行的上面一行,就可以全面攻擊了。按照步驟 1 的方法設置 TCP 連接,打開第三個窗口運行攻擊程序,然後在 TCP 連接的其中一個終端輸入一些字符串,你會發現 TCP 連接被中斷了!

    進一步實驗

    1. 可以繼續使用攻擊程序進行實驗,將偽造數據包的序列號加減 1 看看會發生什麼,是不是確實需要和截獲數據包的 ACK 號完全相同。
    2. 打開 Wireshark,監聽 lo0 網絡接口,並使用過濾器 ip.src == 127.0.0.1 && ip.dst == 127.0.0.1 && tcp.port == 8000 來過濾無關數據。你可以看到 TCP 連接的所有細節。
    3. 在連接上更快速地發送數據流,使攻擊更難執行。

    總的來說,TCP 重置攻擊既深奧又簡單,祝你實驗順利。

    Kubernetes 1.18.2 1.17.5 1.16.9 1.15.12離線安裝包發布地址http://store.lameleg.com ,歡迎體驗。 使用了最新的sealos v3.3.6版本。 作了主機名解析配置優化,lvscare 掛載/lib/module解決開機啟動ipvs加載問題, 修復lvscare社區netlink與3.10內核不兼容問題,sealos生成百年證書等特性。更多特性 https://github.com/fanux/sealos 。歡迎掃描下方的二維碼加入釘釘群 ,釘釘群已經集成sealos的機器人實時可以看到sealos的動態。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 一起玩轉微服務(3)——微服務架構設計模式

    一起玩轉微服務(3)——微服務架構設計模式

    一、聚合器微服務設計模式

    這是一種最常見也最簡單的設計模式,效果如下圖所示。
    聚合器調用多個服務實現應用程序所需的功能。它可以是一個簡單的Web頁面,將檢索到的數據進行處理展示。它也可以是一個更高層次的組合微服務,對檢索到的數據增加業務邏輯後進一步發布成一個新的微服務,這符合DRY原則。另外,每個服務都有自己的緩存和數據庫。如果聚合器是一個組合服務,那麼它也有自己的緩存和數據庫。

    二、代理微服務設計模式

    這是聚合模式的一個變種,如下圖所示。
    在這種情況下,客戶端並不聚合數據,但會根據業務需求的差別調用不同的微服務。代理可以僅僅委派請求,也可以進行數據轉換工作。每個微服務都有自己獨立的緩存和數據庫系統,彼此獨立。

    三、鏈式微服務設計模式

    這種模式在接收到請求後會產生一個經過合併的響應,如下圖所示:
    在這種情況下,服務A接收到請求後會與服務B進行通信,類似地,服務B會同服務C進行通信。所有服務都使用同步消息傳遞。在整個鏈式調用完成之前,客戶端會一直阻塞。
    因此,服務調用鏈不宜過長,以免客戶端長時間等待。

    四、分支微服務設計模式

    這種模式是聚合器模式的擴展,允許同時調用兩個微服務鏈,如下圖所示:
    每個調用鏈分別調用自己的服務。當某個調用出現問題時,互相之間不會造成影響。

    五、數據共享微服務設計模式

    自治是微服務的設計原則之一,就是說微服務是全棧式服務。但在重構現有的“單體應用(monolithic application)”時,SQL數據庫反規範化可能會導致數據重複和不一致。
    因此,在單體應用到微服務架構的過渡階段,可以使用這種設計模式,如下圖所示:
    在這種情況下,部分微服務可能會共享緩存和數據庫存儲。不過,這隻有在兩個服務之間存在強耦合關係時才可以。對於基於微服務的新建應用程序而言,這是一種反模式。
     

    六、異步消息傳遞微服務設計模式

    雖然REST設計模式非常流行,但它是同步的,會造成阻塞。因此部分基於微服務的架構可能會選擇使用消息隊列代替REST請求/響應,如下圖所示:
    各個服務之間通過異步的消息隊列進行交互,當服務出現問題時,不會造成阻塞,隊列會幫助緩存消息,直到消費服務開始工作。

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • 大數據篇:一文讀懂@數據倉庫

    大數據篇:一文讀懂@數據倉庫

    大數據篇:一文讀懂@數據倉庫

    1 網絡詞彙總結

    • 人工智能層的:智慧地球、智慧城市、智慧社會
    • 企業層面的:数字互聯網,数字經濟、数字平台、数字城市、数字政府;
    • 平台層面的:物聯網,雲計算,大數據,5G,人工智能,機器智能,深度學習,知識圖譜
    • 技術層面的:數據倉庫、數據集市、大數據平台、數據湖、數據中台、業務中台、技術中台等等

    挑重點簡介

    1.1 數據中台

    1. 數據中台是聚合和治理跨域數據,將數據抽象封裝成服務,提供給前台以業務價值的邏輯概念。

    2. 數據中台是一套可持續“讓企業的數據用起來”的機制,一種戰略選擇和組織形式,是依據企業特有的業務模式和組織架構,通過有形的產品和實施方法論支撐,構建一套持續不斷把數據變成資產並服務於業務的機制。

    3. 數據中台連接數據前台和後台,突破數據局限,為企業提供更靈活、高效、低成本的數據分析挖掘服務,避免企業為滿足具體某部門某種數據分析需求而投放大量高成本、重複性的數據開發成本。

    4. 數據中台是指通過數據技術,對海量數據進行採集、計算、存儲、加工,同時統一標準和口徑。數據中台把數據統一之後,會形成標準數據,再進行存儲,形成大數據資產層,進而為客戶提供高效服務。

    5. 數據中台,包括平台、工具、數據、組織、流程、規範等一切與企業數據資產如何用起來所相關的。

    可以看出,數據中台是解決如何用好數據的問題,目前還缺乏一個標準,而說到數據中台一定會提及大數據,而大數據又是由數據倉庫發展起來的。

    1.1.1 數據倉庫(Data WareHouse)

    1. 數據倉庫,按照傳統的定義,數據倉庫是一個面向主題的、集成的、非易失的、反映歷史變化(隨時間變化),用來支持管理人員決策的數據集合。

    為企業所有決策制定過程,提供所有系統數據支持的戰略集合

    • 面向主題

    操作型數據庫的數據組織面向事務處理任務,各個業務系統之間各自分離,而數據倉庫中的數據是按照一定的主題域進行組織。

    主題是一個抽象的概念,是數據歸類的標準,是指用戶使用數據倉庫進行決策時所關心的重點方面,一個主題通常與多個操作型信息系統相關。每一個主題基本對應一個宏觀的分析領域。

    例如,銀行的數據倉庫的主題:客戶

    客戶數據來源:從銀行儲蓄數據庫、信用卡數據庫、貸款數據庫等幾個數據庫中抽取的數據整理而成。這些客戶信息有可能是一致的,也可能是不一致的,這些信息需要統一整合才能完整體現客戶。

    • 集成

    面向事務處理的操作型數據庫通常與某些特定的應用相關,數據庫之間相互獨立,並且往往是異構的。而數據倉庫中的數據是在對原有分散的數據庫數據抽取、清理的基礎上經過系統加工、匯總和整理得到的,必須消除源數據中的不一致性,以保證數據倉庫內的信息是關於整個企業的一致的全局信息。

    具體如下:

    1:數據進入數據倉庫后、使用之前,必須經過加工與集成。

    2:對不同的數據來源進行統一數據結構和編碼。統一原始數 據中的所有矛盾之處,如字段的同名異義,異名同義,單位不統一,字長不一致等。

    3:將原始數據結構做一個從面嚮應用到面向主題的大轉變。

    • 非易失即相對穩定的

    操作型數據庫中的數據通常實時更新,數據根據需要及時發生變化。數據倉庫的數據主要供企業決策分析之用,所涉及的數據操作主要是數據查詢,一旦某個數據進入數據倉庫以後,一般情況下將被長期保留,也就是數據倉庫中一般有大量的查詢操作,但修改和刪除操作很少,通常只需要定期的加載、刷新。

    數據倉庫中包括了大量的歷史數據。

    數據經集成進入數據倉庫后是極少或根本不更新的。

    • 隨時間變化即反映歷史變化

    操作型數據庫主要關心當前某一個時間段內的數據,而數據倉庫中的數據通常包含歷史信息,系統記錄了企業從過去某一時點(如開始應用數據倉庫的時點)到目前的各個階段的信息,通過這些信息,可以對企業的發展歷程和未來趨勢做出定量分析和預測。企業數據倉庫的建設,是以現有企業業務系統和大量業務數據的積累為基礎。數據倉庫不是靜態的概念,只有把信息及時交給需要這些信息的使用者,供他們做出改善其業務經營的決策,信息才能發揮作用,信息才有意義。而把信息加以整理歸納和重組,並及時提供給相應的管理決策人員,是數據倉庫的根本任務。因此,從產業界的角度看,數據倉庫建設是一個工程,是一個過程

    數據倉庫內的數據時限一般在5-10年以上,甚至永不刪除,這些數據的鍵碼都包含時間項,標明數據的歷史時期,方便做時間趨勢分析。

    1. 數據倉庫,並不是數據最終目的地,而是為數據最終的目的地做好準備:清洗、轉義、分類、重組、合併、拆分、統計等等

    通過對數據倉庫中數據的分析,可以幫助企業,改進業務流程、控制、成本、提高產品質量等

    1. 主要解決問題:數據報表,數據沉澱,數據計算Join過多,數據查詢過慢等問題。

    防止煙囪式開發,減少重複開發,開發通用中間層數據,減少重複計算;

    將複雜問題簡單化,將複雜任務的多個步驟分解到各個層次中,每一層只處理較少的步驟,使單個任務更容易理解;

    可進行數據血緣追蹤,便於快速定位問題;

    整個數據層次清晰,每個層次的數據都有職責定位,便於使用和理解。

    1. 主要價值體現:企業數據模型,這些模型隨着前端業務系統的發展變化,不斷變革,不斷追加,不斷豐富和完善,即使系統不再了,也可以在短期內快速重建起來,這也是大數據產品能夠快速迭代起來的一個重要原因

    總結:數據倉庫,即為企業數據的模型沉澱,為了能更快的發展大數據應用,提供可靠的模型來快速迭代。本文也主要為了講解數據倉庫

    • 數倉硬件架構圖
    • 數倉功能架構圖
    • 數倉流程架構圖1
    • 數倉流程架構圖2
    • 實時數倉流程架構圖

    1.1.2 大數據平台(DATA Platform)

    1. 大數據平台則是指以處理海量數據存儲、計算及流數據實時計算等場景為主的一套基礎設施,包括了統一的數據採集中心、數據計算和存儲中心、數據治理中心、運維管控中心、開放共享中心和應用中心。

    2. 大數據平台的建設出發點是節約投資降低成本,但實際上無論從硬件投資還是從軟件開發上都遠遠超過數據倉庫的建設,大量的硬件和各種開源技術的組合,增加了研發的難度、調測部署的周期、運維的複雜度,人力上的投入已是最初的幾倍;還有很多技術上的困難也非一朝一夕能夠突破。

    3. 首先是數據的應用問題,無論是數據倉庫還是大數據平台,裡面包含了接口層數據、存儲層數據、輕度匯總層、重度匯總層、模型層數據、報表層數據等等,各種各樣的表有成千上萬,這些表有的是中間處理過程,有些是一次性的報表,不同表之間的數據一致性和口徑也會不同,而且不同的表不同的字段對數據安全要求級別也不同。

    4. 此外還要考慮多租戶的資源安全管理,如何讓內部開發者快速獲取所需的數據資產目錄,如何閱讀相關數據的來龍去脈,如何快速的實現開發,這些在大數據平台建設初期沒有考慮周全。

    5. 另外一個問題是對外應用,隨着大數據平台的應用建設,每一個對外應用都採用單一的數據庫加單一應用建設模式,獨立考慮網絡安全、數據安全、共享安全,逐漸又走向了煙囪似的開發道路。

    總結:大數據平台,即為數據一站式服務,提供可視化的數據展示,提取,計算任務安排,資源管理,數據治理,安全措施,共享應用等等。

    • 平台數據流向圖
    • 平台流程架構圖

    1.1.3 數據中台(Data Middle Platform)

    1. 數據中台要解決什麼?數據如何安全的、快速的、最小權限的、且能夠溯源的被探測和快速應用的問題。

    2. 數據中台不應該被過度的承載平台的計算、存儲、加工任務,而是應該放在解決企業邏輯模型的搭建和存儲、數據標準的建立、數據目錄的梳理、數據安全的界定、數據資產的開放,知識圖譜的構建。

    3. 通過一系列工具、組織、流程、規範,實現數據前台和後台的連接,突破數據局限,為企業提供更靈活、高效、低成本的數據分析挖掘服務,避免企業為滿足具體某部門某種數據分析需求而投放大量高成本、重複性的數據開發成本。

    總結:厚平台,大中台,小前台;沒有基礎厚實笨重的大數據平台,是不可能構建數據能力強大、功能強大的數據中台的;沒有大數據中台,要迅速搭建小快靈的小前台也只是理想化的。

    • 中台架構圖
    • 阿里數據中台架構圖

    2 數據庫的”分家”

    隨着關係數據庫理論的提出,誕生了一系列經典的RDBMS,如Oracle,MySQL,SQL Server等。這些RDBMS被成功推向市場,併為社會信息化的發展做出的重大貢獻。然而隨着數據庫使用範圍的不斷擴大,它被逐步劃分為兩大基本類型:

    1. 操作型數據庫(OLTP)

    主要用於業務支撐。一個公司往往會使用並維護若干個數據庫,這些數據庫保存着公司的日常操作數據,比如商品購買、酒店預訂、打車下單、外賣訂購等;

    1. 分析型數據庫(OLAP)

    主要用於歷史數據分析。這類數據庫作為公司的單獨數據存儲,負責利用歷史數據對公司各主題域進行統計分析;

    • 總結

    那麼為什麼要”分家”?在一起不合適嗎?能不能構建一個同樣適用於操作和分析的統一數據庫?

    答案是NO。一個顯然的原因是它們會”打架”……如果操作型任務和分析型任務搶資源怎麼辦呢?再者,它們有太多不同,以致於早已”貌合神離”。接下來看看它們到底有哪些不同吧。

    因為主導功能的不同(面向操作/面向分析),兩類數據庫就產生了很多細節上的差異。就好像玩LOL一个中單一個ADC,肯定有很多行為/觀念上的不同

    2.1 OLAP 和 OLTP簡介

    數據處理大致可以分成兩大類:

    聯機事務處理OLTP(on-line transaction processing):是傳統的關係型數據庫的主要應用,主要是基本的、日常的事務處理,例如銀行交易。系統強調數據庫內存效率,強調內存各種指標的命令率,強調綁定變量,強調併發操作。

    聯機分析處理OLAP(On-Line Analytical Processing):是數據倉庫系統的主要應用,支持複雜的分析操作,側重決策支持,並且提供直觀易懂的查詢結果。 系統則強調數據分析,強調SQL執行市場,強調磁盤I/O,強調分區等。

    2.2 定義差別

    對比內容 操作型數據庫(OLTP) 分析型數據庫(OLAP)
    數據內容 當前值 歷史的、存檔的、歸納的、計算的數據
    數據目標 面向業務操作程序,重複處理 面向主題域,分析應用,支持決策
    數據特性 動態變化,按字段更新 靜態、不能直接更新,只能定時添加、刷新
    數據結構 高度結構化、複雜,適合操作計算 簡單,適合分析
    使用頻率 中到低
    數據訪問量 每個事務只訪問少量記錄 有的事務可能需要訪問大量記錄
    對響應時間的要求 以秒為單位計算 以秒、分鐘、甚至小時為計算單位

    2.3 定位差別

    對比屬性 OLTP OLAP
    代表 Mysql Hive
    讀特性 每次查詢只返回少量數據 對大量數據進行匯總
    寫特性 隨機、低延遲寫入用戶的操作 批量導入
    用戶 操作人員 決策人員
    DB設計 面嚮應用 面向主題
    數據 當前的,最新的細節,二維表 歷史的,聚集的,多維表
    工作單位 事務性保證 複雜查詢
    用戶數 上千個 上百萬個
    DB大小 100MB-GB 100GB-TB以上
    時間要求 具有實時性 對時間的要求不嚴格
    主要應用 數據庫:WEB項目 數據倉庫:分析師,挖掘師

    2.4 組成差別

    對比內容 操作型數據庫(OLTP) 分析型數據庫(OLAP)
    數據時間範圍差別 只會存放一定天數的數據 存放的則是數年內的數據
    數據細節層次差別 存放的主要是細節數據 也有匯總需求,但匯總數據本身不存儲而只存儲其生成公式。
    這是因為操作型數據是動態變化的,因此匯總數據會在每次查詢時動態生成。
    存放的既有細節數據,又有匯總數據,對於用戶來說,重點關注的是匯總數據部分。
    因為匯總數據比較穩定不會發生改變,而且其計算量也比較大(因為時間跨度大),因此它的匯總數據可考慮事先計算好,以避免重複計算。
    數據時間表示差別 通常反映的是現實世界的當前狀態 既有當前狀態,還有過去各時刻的快照。
    可以綜合所有快照對各個歷史階段進行統計分析

    2.5 技術差別

    對比內容 操作型數據庫(OLTP) 分析型數據庫(OLAP)
    數據更新差別 允許用戶進行增,刪,改,查 規範是只能進行查詢
    數據冗餘差別 減少數據冗餘,避免更新異常 沒有更新操作。因此,減少數據冗餘也就沒那麼重要了

    2.6 功能差別

    對比內容 操作型數據庫(OLTP) 分析型數據庫(OLAP)
    數據讀者差別 使用者是業務環境內的各個角色,如用戶,商家,進貨商等 只被少量用戶(高級管理者)用來做綜合性決策
    數據定位差別 是為了支撐具體業務創建的,因此也被稱為”面嚮應用型數據庫” 是針對各特定業務主題域的分析任務創建的,因此也被稱為”面向主題型數據庫”

    2.7 OLTP數據庫三範式介紹

    • 定義:範式可以理解為設計一張數據表的表結構,符合的標準級別。 規範和要求
    • 優點:關係型數據庫設計時,遵照一定的規範要求,目的在於降低數據的冗餘性。
      • 十幾年前,磁盤很貴,為了減少磁盤存儲。
      • 以前沒有分佈式系統,都是單機,只能增加磁盤,磁盤個數也是有限的
      • 一次修改,需要修改多個表,很難保證數據一致性
    • 缺點:範式的缺點是獲取數據時,需要通過 Join 拼接出最後的數據。

    目前業界範式有:第一範式(1NF)、第二範式(2NF)、第三範式(3NF)、巴斯-科德範式 (BCNF)、第四範式(4NF)、第五範式(5NF)。

    2.7.1 函數依賴

    學號 姓名 系名 班主任 課名 分數
    001 張三 古文系 李白 文言文 89
    001 張三 古文系 李白 古詩詞 78
    001 張三 古文系 李白 現代漢語 65
    002 李四 古文系 李白 文言文 45
    002 李四 古文系 李白 古詩詞 78
    002 李四 古文系 李白 甲骨文 98
    003 王五 數學系 牛頓 高等數學 88
    003 王五 數學系 牛頓 數學基礎 88
    1. 完全函數依賴:

    通過 AB 能推出 C,但是 AB 單獨得不到 C,那麼可以說:C 完全依賴於 AB

    (學號,課名)推出 分數,但是 單獨用學號 推不出 分數,那麼可以說:分數 完全依賴於(學號,課名)

    1. 部分函數依賴:

    通過 AB 能推出 C,通過 單獨的A 或者 單獨的B 也能推出 C,那麼可以說:C 部分依賴於 AB

    (學號,課名)推出 姓名,而還可以通過 學號 直接推出 姓名,那麼可以說:姓名 部分依賴於(學號,課名)

    1. 傳遞函數依賴:

    通過 A 得到 B,通過 B 得到 C,但是通過 C 不能得到 A,那麼可以說:C 傳遞依賴於 A

    通過 學號 推出 系名,系名 推出 系主任,但是 系主任 不能推出 學號,那麼可以說:系主任 專遞依賴於 學號

    2.7.2 三範式區分

    2.7.2.1 第一範式:屬性不可切割
    • 不符合第一範式表設計
    ID 商品 商家ID 用戶ID
    001 5台電腦 小米_001 00001

    如上表格不符合第一範式,商品列中的數據不是原子數據項,是可以進行分割的。

    • 符合第一範式表設計
    ID 商品 數量 商家ID 用戶ID
    001 電腦 5 小米_001 00001

    1NF是所有關係數據庫的最基本要求

    2.7.2.2 第二範式:不能存在”部分函數依賴”
    • 不符合第二範式表設計
    學號 姓名 系名 班主任 課名 分數
    001 張三 古文系 李白 文言文 89
    001 張三 古文系 李白 古詩詞 78
    001 張三 古文系 李白 現代漢語 65
    002 李四 古文系 李白 文言文 45
    002 李四 古文系 李白 古詩詞 78
    002 李四 古文系 李白 甲骨文 98
    003 王五 數學系 牛頓 高等數學 88
    003 王五 數學系 牛頓 數學基礎 88

    如上表格不符合第二範式,比如:這張表主鍵(學號,課名),分數完全依賴於(學號和課名),但是姓名並不完全依賴於(學號和課名)

    • 符合第二範式表設計
    學號 課名 分數
    001 文言文 89
    001 古詩詞 78
    001 現代漢語 65
    002 文言文 45
    002 古詩詞 78
    002 甲骨文 98
    003 高等數學 88
    003 數學基礎 88
    學號 姓名 系名 班主任
    001 張三 古文系 李白
    002 李四 古文系 李白
    003 王五 數學系 牛頓
    2.7.2.3 第三範式:不能存在”傳遞函數依賴”
    • 不符合第三範式表設計
    學號 姓名 系名 班主任
    001 張三 古文系 李白
    002 李四 古文系 李白
    003 王五 數學系 牛頓

    如上表格不符合第三範式,比如:學號–>系名–>系主任,但是系主任推不出學號

    • 符合第三範式表設計
    學號 姓名 系名
    001 張三 古文系
    002 李四 古文系
    003 王五 數學系
    系名 班主任
    古文系 李白
    古文系 李白
    數學系 牛頓

    2.8 OLAP典型架構

    OLAP有多種實現方法,根據存儲數據的方式不同可以分為ROLAP、MOLAP、HOLAP

    名稱 描述 細節數據存儲位置 聚合后的數據存儲位置
    ROLAP(Relational OLAP) 基於關係數據庫的OLAP實現 關係型數據庫 關係型數據庫
    MOLAP(Multidimensional OLAP) 基於多維數據組織的OLAP實現 數據立方體 數據立方體
    HOLAP(Hybrid OLAP) 基於混合數據組織的OLAP實現 關係型數據庫 數據立方體
    1. ROLAP(Relational Online Analytical Processing)

    ROLAP架構並不會生成實際的多維數據集,而是使用雪花模式以及多個關係表對數據立方體進行模擬,它的OLAP引擎就是將用戶的OLAP操作,如上鑽下鑽過濾合併等,轉換成SQL語句提交到數據庫中執行,並且提供聚集導航功能,根據用戶操作的維度和度量將SQL查詢定位到最粗粒度的事實表上去

    這種架構下的查詢沒有MOLAP快速。因為ROLAP中,所有的查詢都是被轉換為SQL語句執行的。而這些SQL語句的執行會涉及到多個表之間的JOIN操作,沒有MOLAP速度快,往往都是通過內存計算實現。(內存的昂貴大家是知道的)

    1. MOLAP(Multidimensional Online Analytical Processing)

    MOLAP架構會生成一個新的多維數據集,也可以說是構建了一個實際數據立方體。事先將匯總數據計算好,存放在自己特定的多維數據庫中,用戶的OLAP操作可以直接映射到多維數據庫的訪問,不通過SQL訪問。(空間換時間,典型代表Kylin)

    在該立方體中,每一格對應一個直接地址,且常用的查詢已被預先計算好。因此每次的查詢都是非常快速的,但是由於立方體的更新比較慢,所以是否使用這種架構得具體問題具體分析。

    1. HOLAP(Hybrid Online Analytical Processing)

    這種架構綜合參考MOLAP和ROLAP而採用一種混合解決方案,將某些需要特別提速的查詢放到MOLAP引擎,其他查詢則調用ROLAP引擎。上述MOLAP和ROLAP的結合。它提供了更大的靈活度,MOLAP提供提供了更加快速的響應速度。但是帶來的問題是,數據裝載的效率非常低,因為其實就是將多維的數據預先填好,但是隨着數據量過大維度成本越高,容易引起“數據爆炸”。

    2.9 OLAP數據立方體(Data Cube)

    OLAP(online analytical processing)是一種軟件技術,它使分析人員能夠迅速、一致、交互地從各個方面觀察信息,以達到深入理解數據的目的。從各方面觀察信息,也就是從不同的維度分析數據,因此OLAP也稱為多維分析

    很多年前,當我們要手工從一堆數據中提取信息時,我們會分析一堆數據報告。通常這些數據報告採用二維表示,是行與列組成的二維表格。但在真實世界里我們分析數據的角度很可能有多個,數據立方體可以理解為就是維度擴展后的二維表格。下圖展示了一個三維數據立方體:

    更多時候數據立方體是N維的。它的實現有兩種方式。其中星形模式就是其中一種,該模式其實是一種連接關係表與數據立方體的橋樑。但對於大多數純OLAP使用者來講,數據分析的對象就是這個邏輯概念上的數據立方體,其具體實現不用深究。對於這些OLAP工具的使用者來講,基本用法是首先配置好維表、事實表,然後在每次查詢的時候告訴OLAP需要展示的維度和事實字段和操作類型即可。

    最常見的五大操作:切片,切塊,旋轉,上卷,下鑽。

    2.9.1 切片和切塊(Slice and Dice)

    在數據立方體的某一維度上選定一個維成員的操作叫切片,而對兩個或多個維執行選擇則叫做切塊。下圖邏輯上展示了切片和切塊操作:

    2.9.2 旋轉(Pivot)

    旋轉就是指改變報表或頁面的展示方向。對於使用者來說,就是個視圖操作,而從SQL模擬語句的角度來說,就是改變SELECT後面字段的順序而已。下圖邏輯上展示了旋轉操作:

    2.9.3 上卷和下鑽(Rol-up and Drill-down)

    上卷可以理解為”無視”某些維度;下鑽則是指將某些維度進行細分。下圖邏輯上展示了上卷和下鑽操作:

    2.9.4 Cube 和 Cuboid

    Cube(或 Data Cube),即數據立方體,是一種常用於數據分析與索引的技術;它可以對原始數據建立多維度索引。通過 Cube 對數據進行分析,可以大大加快數據的查詢效率。

    Cuboid 特指在某一種維度組合下所計算的數據。 給定一個數據模型,我們可以對其上的所有維度進行組合。對於 N 個維度來說,組合的所有可能性共有 2 的 N 次方種。對於每一種維度的組合,將度量做 聚合運算,然後將運算的結果保存為一個物化視圖,稱為 Cuboid。

    所有維度組合的 Cuboid 作為一個整體,被稱為 Cube。所以簡單來說,一個 Cube 就是許多按維度聚合的物化視圖的集合。

    下面來列舉一個具體的例子:

    假定有一個電商的銷售數據集,其中維度包括 時間(Time)、商品(Item)、地點(Location)和供應商(Supplier),度量為銷售額(GMV)。

    • 那麼所有維度的組合就有 2 的 4 次方 =16 種
      • 一維度(1D) 的組合有[Time]、[Item]、[Location]、[Supplier]4 種
      • 二維度(2D)的組合 有[Time,Item]、[Time,Location]、[Time、Supplier]、[Item,Location]、 [Item,Supplier]、[Location,Supplier]6 種
      • 三維度(3D)的組合也有 4 種
      • 零維度(0D)的組合有 1 種
      • 四維度(4D)的組合有 1 種

    3 數據倉庫的演進

    4 數據倉庫主要用途

    大家應該已經意識到這個問題:既然分析型數據庫中的操作都是查詢,因此也就不需要嚴格滿足完整性/參照性約束以及範式設計要求,而這些卻正是分析型數據庫精華所在。這樣的情況下再將它歸為數據庫會很容易引起大家混淆,畢竟在絕大多數人心裏數據庫是可以關係型數據庫畫上等號的。

    • 那麼為什麼不幹脆叫”面向分析的存儲系統”呢?

    這就是關於數據倉庫最貼切的定義了。事實上數據倉庫不應讓傳統關係數據庫來實現,因為關係數據庫最少也要求滿足第1範式,而數據倉庫里的關係表可以不滿足第1範式。也就是說,同樣的記錄在一個關係表裡可以出現N次。但由於大多數數據倉庫內的表的統計分析還是用SQL,因此很多人把它和關係數據庫搞混了。

    4.1 支持數據提取

    數據提取可以支撐來自企業各業務部門的數據需求。

    由之前的不同業務部門給不同業務系統提需求轉變為不同業務系統統一給數據倉庫提需求,避免煙囪式開發

    4.2 支持報表系統

    基於企業的數據倉庫,向上支撐企業的各部門的統計報表需求,輔助支撐企業日常運營決策。

    4.3 支持數據分析

    從許多來自不同的企業業務系統的數據中提取出有用的數據並進行清理,以保證數據的正確性,然後經過抽取、轉換和裝載,即ETL過程,合併到一個企業級的數據倉庫里,從而得到企業數據的一個全局視圖;

    在此基礎上利用合適的查詢和分析工具、數據挖掘工具、OLAP工具等對其進行分析和處理(這時信息變為輔助決策的知識);

    最後將知識呈現給管理者,為管理者的決策過程提供支持 。

    4.4 支持數據挖掘

    數據挖掘也稱為數據庫知識發現(Knowledge Discovery in Databases, KDD),就是將高級智能計算技術應用於大量數據中,讓計算機在有人或無人指導的情況下從海量數據中發現潛在的,有用的模式(也叫知識)。

    Jiawei Han在《數據挖掘概念與技術》一書中對數據挖掘的定義:數據挖掘是從大量數據中挖掘有趣模式和知識的過程,數據源包括數據庫、數據倉庫、Web、其他信息存儲庫或動態地流入系統的數據。

    4.5 支持數據應用

    物聯網基於位置數據的旅遊客流分析及人群畫像

    通信基於位置數據的人流監控和預警

    銀行基於用戶交易數據的金融畫像應用

    電商根據用戶瀏覽和購買行為的用戶標籤體系及推薦系統

    徵信機構根據用戶信用記錄的信用評估

    出行基於位置數據的車流量分析,調度預測

    5 數據集市

    數據集市可以理解為是一種”小型數據倉庫”,它只包含單個主題,且關注範圍也非全局。

    數據集市可以分為兩種,一種是獨立數據集市(independent data mart),這類數據集市有自己的源數據庫和ETL架構;另一種是非獨立數據集市(dependent data mart),這種數據集市沒有自己的源系統,它的數據來自數據倉庫。當用戶或者應用程序不需要/不必要/不允許用到整個數據倉庫的數據時,非獨立數據集市就可以簡單為用戶提供一個數據倉庫的”子集”。

    • 簡單理解:
      • 數據集市:部門級別的數據倉庫,能為某個局部範圍內的管理人員提供服務。
      • 數據倉庫:企業級別的數據倉庫,能為企業各個部門的運行提供決策支持。

    6 建模的基本概念

    6.1 關係建模

    上圖為web應用中的一個建模片段,遵循三範式建模,可以看出,較為鬆散、零碎, 物理表數量多,而數據冗餘程度低。由於數據分佈於眾多的表中,這些數據可以更為靈活地 被應用,功能性較強。關係模型主要應用與 OLTP 系統中,為了保證數據的一致性以及避免 冗餘,所以大部分業務系統的表都是遵循第三範式的。

    6.2 維度建模

    維度建模(dimensional modeling)是專門用於分析型數據庫、數據倉庫、數據集市建模的方法

    上圖為維度模型建模片段,主要應用於 OLAP 系統中,通常以某一個事實表為中心進行表的 組織,主要面向業務,特徵是可能存在數據的冗餘,但是能方便的得到數據。

    關係模型雖然冗餘少,但是在大規模數據,跨表分析統計查詢過程中,會造成多表關聯,這會大大降低執行效率。所以通常我們採用維度模型建模,把相關各種表整理成兩種: 事實表和維度表兩種

    6.3 維度建模的三種模式

    1. 星形模式

    星形模式(Star Schema)是最常用的維度建模方式

    可以看出,星形模式的維度建模由一個事實表和一組維表成,且具有以下特點:

    1. 維表只和事實表關聯,維表之間沒有關聯;
    2. 每個維表的主碼為單列,且該主碼放置在事實表中,作為兩邊連接的邏輯外鍵;
    3. 以事實表為核心,維表圍繞核心呈星形分佈;
    1. 雪花模式

    雪花模式(Snowflake Schema)是對星形模式的擴展,每個維表可繼續向外連接多個子維表。(三範式代表作)

    星形模式中的維表相對雪花模式來說要大,而且不滿足規範化設計。雪花模型相當於將星形模式的大維表拆分成小維表,滿足了規範化設計。然而這種模式在實際應用中很少見,因為這樣做會導致開發難度增大,而數據冗餘問題在數據倉庫里並不嚴重。

    1. 星座模式

    星座模式(Fact Constellations Schema)也是星型模式的擴展。

    前面兩種維度建模方法都是多維表對應單事實表,但在很多時候維度空間內的事實表不止一個,而一個維表也可能被多個事實表用到。在業務發展後期,星座模式將作為最主要的維度建模。

    6.4 維度表和事實表

    1. 維度表(dimension)

    表示對分析主題所屬類型的描述。比如”昨天早上張三在京東花費200元購買了一個皮包”。那麼以購買為主題進行分析,可從這段信息中提取三個維度:時間維度(昨天早上),地點維度(京東), 商品維度(皮包)。通常來說維度表信息比較固定,且數據量小。

    1. 事實表(fact table)

    表示對分析主題的度量。比如上面那個例子中,200元就是事實信息。事實表包含了與各維度表相關聯的邏輯外鍵,並通過JOIN方式與維度表關聯。事實表的度量通常是數值類型,且記錄數會不斷增加,表規模迅速增長。

    1. 事實維度舉例

    昨天我去菜市場買了一隻蝙蝠,然後我就被隔離了。

    • 事實:訂單==>買蝙蝠這個事

    • 維度:

      • 時間==>昨天
      • 用戶==>我
      • 商品==>蝙蝠
      • 地理==>菜市場

    6.4.1 維度表

    維度表:一般是對事實的描述信息。每一張維表對應現實世界中的一個對象或者概念。 例如:用戶、商品、日期、地區等。

    常用於一個客觀世界的維度描述,往往列比較多。

    審視數據的角度

    • 維表的特徵:
      • 維表的範圍很寬(具有多個屬性、列比較多)
      • 跟事實表相比,行數相對較小:通常< 10 萬條
      • 靜態表示的,名詞性質的表

    6.4.2 事實表

    事實表用於正確的記錄既定的已經發生的事實,常用於存儲ID和度量值,各種維度外鍵

    事實表中的每行數據代表一個業務事件(下單、支付、退款、評價等)。“事實”這 個術語表示的是業務事件的度量值(可統計次數、個數、件數、金額等),例如,訂單事件中的下單金額。

    每一個事實表的行包括:具有可加性的數值型的度量值、與維表相連接的外鍵、通常具 有兩個和兩個以上的外鍵、外鍵之間表示維表之間多對多的關係。

    • 事實表的特徵:
      • 非常的大
      • 內容相對的窄:列數較少
      • 經常發生變化,每天會新增加很多
      • 動態表示的,動詞性質的表
    1. 事務型事實表(每天導入新增)
      • 以每個事務或事件為單位,例如一個銷售訂單記錄,一筆支付記錄等,作為事實表裡的 一行數據。一旦事務被提交,事實表數據被插入,數據就不再進行更改,其更新方式為增量 更新
    2. 周期型快照事實表(每日全量)
      • 周期型快照事實表中不會保留所有數據,只保留固定時間間隔的數據,例如每天或者 每月的銷售額,或每月的賬戶餘額等
    3. 累積型快照事實表(每天導入新增及變化)
      • 累計快照事實表用於跟蹤業務事實的變化。例如,數據倉庫中可能需要累積或者存儲 訂單從下訂單開始,到訂單商品被打包、運輸、和簽收的各個業務階段的時間點數據來跟蹤 訂單聲明周期的進展情況。當這個業務過程進行時,事實表的記錄也要不斷更新。

    6.5 數據分層

    • 為什麼分層:
      • 簡單化:把複雜的任務分解為多層來完成,每層處理各自的任務,方便定位問題。
      • 減少重複開發:規範數據分層,通過中間層數據,能夠極大的減少重複計算,增加結果復用性。
      • 隔離數據:不論是數據異常還是數據敏感性,使真實數據和統計數據解耦。

    下面列舉常見電商表的分層結構

    6.5.1 ODS層

    • 保持數據原貌不做任何修改,起到備份數據的作用。
    • 數據採用壓縮,減少磁盤存儲空間(例如:原始數據 100G,可以壓縮到 10G 左 右)
    • 創建分區表,防止後續的全表掃描

    6.5.2 DWD層

    DWD 層需構建維度模型,一般採用星型模型,呈現的狀態一般為星座模型。

    • 維度建模一般按照四個步驟: 選擇業務過程→聲明粒度→確認維度→確認事實

    • 選擇業務過程

      • 在業務系統中,挑選我們感興趣的業務線,比如下單業務,支付業務,退款業務,物流 業務,一條業務線對應一張事實表。
    • 聲明粒度

      • 數據粒度指數據倉庫的數據中保存數據的細化程度或綜合程度的級別。

      • 聲明粒度意味着精確定義事實表中的一行數據表示什麼,應該盡可能選擇最小粒度,以 此來應各種各樣的需求。

      • 典型的粒度聲明如下:

        • 訂單中,每個商品項作為下單事實表中的一行,粒度為每次下單
        • 每周的訂單次數作為一行,粒度就是每周下單。
        • 每月的訂單次數作為一行,粒度就是每月下單
    • 確定維度

      • 維度的主要作用是描述業務是事實,主要表示的是“誰,何處,何時”等信息。
    • 確定事實

      • 此處的“事實”一詞,指的是業務中的度量值,例如訂單金額、下單次數等。
      • 在 DWD 層,以業務過程為建模驅動,基於每個具體業務過程的特點,構建最細粒度的 明細層事實表。事實表可做適當的寬表化處理。
    事實/維度 時間 用戶 地區 商品 優惠卷 活動 編碼 度量
    訂單 件數/金額
    訂單詳情 件數/金額
    支付 次數/金額
    加入購物車 件數/金額
    收藏 個數
    評價 個數
    退款 件數/金額
    優惠卷領用 個數

    6.5.3 DWS層

    • 統計各個主題對象的當天行為,服務於 DWT 層的主題寬表,以及一些業務明細數據, 應對特殊需求(例如,購買行為,統計商品復購率)。

    6.5.4 DWT層

    • 以分析的主題對象為建模驅動,基於上層的應用和產品的指標需求,構建主題對象的全 量寬表。(就是按照維度來決定分析者的角度,如用戶->什麼時間->下了什麼單,支付了什麼,加入購物車了什麼)

    6.5.5 ADS層

    對系統各大主題指標分別進行分析。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • JPS/JPS+ 尋路算法

    JPS/JPS+ 尋路算法

    目錄

    • 概念
      • 強迫鄰居(Forced Neighbour)
      • 跳點(Jump Point)
    • JPS 尋路算法(Jump Point Search)
      • 實現原理
      • 示例過程
    • JPS+(Jump Point Search Plus)
      • 預處理
      • 示例過程
    • 總結
    • 參考

    概念

    JPS(jump point search)算法實際上是對A* 尋路算法的一個改進,因此在閱讀本文之前需要先了解A*算法。A* 算法在擴展節點時會把節點所有鄰居都考慮進去,這樣openlist中點的數量會很多,搜索效率較慢。

    若不了解A*算法,可以參考博主以前寫的一篇文章 A* 尋路算法 – KillerAery – 博客園

    例如在無遮擋情況下(往往會有多條等價路徑),而我們希望起點到終點實際只取其中一條路徑,而該路徑外其它節點可以沒必要放入openlist(不希望加入沒必要的鄰居)。

    其次我們還希望直線方向上中途的點不用放入openlist,如果只放入每段直線子路徑的起點和終點,那openlist又可以少放很多沒必要的節點:

    可以看到 JPS 算法搜到的節點總是“跳躍性”的,這是因為這些關鍵性的節點都是需要改變行走方向的拐點,因此這也是 Jump Point 命名的來歷。

    在介紹JPS等算法具體實現前,我們必須先掌握下面的概念。

    強迫鄰居(Forced Neighbour)

    強迫鄰居:節點 x 的8個鄰居中有障礙,且 x 的父節點 p 經過x 到達 n 的距離代價比不經過 x 到達的 n 的任意路徑的距離代價小,則稱 n 是 x 的強迫鄰居。

    看定義也許十分晦澀難懂。直觀來說,實際就是因為前進方向(父節點到 x 節點的方向為前進方向)的某一邊的靠後位置有障礙物,因此想要到該邊靠前的空位有最短的路徑,就必須得經過過 x 節點。

    可能的情況見圖示,黑色為障礙,紅圈即為強迫鄰居:

    (左圖為直線方向情況下的強迫鄰居,右圖為斜方向情況下的強迫鄰居)

    跳點(Jump Point)

    跳點:當前點 x 滿足以下三個條件之一:

    • 節點 x 是起點/終點。
    • 節點 x 至少有一個強迫鄰居。
    • 如果父節點在斜方向(意味着這是斜向搜索),節點x的水平或垂直方向上有滿足條件a,b的點。

    節點y的水平或垂直方向是斜向向量的拆解,比如向量d=(1,1),那麼水平方向則是(1,0),並不會往左搜索,只會看右邊,如果向量d=(-1,-1),那麼水平方向是(-1,0),只會搜索左邊,不看右邊,其他同理。

    下圖舉個例子,由於黃色節點的父節點是在斜方向,其對應分解成向上和向右兩個方向,因為在右方向發現一個藍色跳點,因此黃色節點也應被判斷為跳點:

    JPS 尋路算法(Jump Point Search)

    實現原理

    JPS 算法和A* 算法非常相似,步驟大概如下:

    1. openlist取一個權值最低的節點,然後開始搜索。(這些和A*是一樣的)
    2. 搜索時,先進行 直線搜索(4/8個方向,跳躍搜索),然後再 斜向搜索(4個方向,只搜索一步)。如果期間某個方向搜索到跳點或者碰到障礙(或邊界),則當前方向完成搜索,若有搜到跳點就添加進openlist。

    跳躍搜索是指沿直線方向一直搜下去(可能會搜到很多格),直到搜到跳點或者障礙(邊界)。一開始從起點搜索,會有4個直線方向(上下左右),要是4個斜方向都前進了一步,此時直線方向會有8個。

    1. 若斜方向沒完成搜索,則斜方向前進一步,重複上述過程。

    因為直線方向是跳躍式搜索,所以總是能完成搜索。

    1. 若所有方向已完成搜索,則認為當前節點搜索完畢,將當前節點移除於openlist,加入closelist。
    2. 重複取openlist權值最低節點搜索,直到openlist為空或者找到終點。

    下面結合圖片更好說明過程2和3:首先我們從openlist取出綠色的節點,作為搜索的開始,先進行直線搜索,再斜向搜索,沒有找到任何跳點。

    斜方向前進一步后,重複直線搜索和斜向搜索過程,仍沒發現跳點。

    斜方向前進兩步后,重複直線搜索和斜向搜索過程,仍沒發現跳點。

    斜方向前進了三步后(假設當前位置為 x),在水平直線搜索上發現了一個跳點(紫色節點為強迫鄰居)。

    於是 x 也被判斷為跳點,添加進openlist。斜方向結束,綠色節點的搜索過程也就此結束,被移除於openlist,放入closelist。

    示例過程

    下面展示JPS算法更加完整的過程:
    假設起點為綠色節點,終點為紅色節點

    重複直線搜索和斜向搜索過程,斜方向前進了3步。在第3步判斷出黃色節點為跳點(依據是水平方向有其它跳點),將黃色跳點放入openlist,然後斜方向搜索完成,綠色節點移除於openlist,放入closelist。

    對openlist下一個權值最低的節點(即黃色節點)開啟搜索,在直線方向上發現了藍色節點為跳點(依據是紫色節點為強迫鄰居),類似地,放入openlist。

    由於斜方向還沒結束,繼續前進一步。最後一次直線搜索和斜向搜索都碰到了邊界,因此黃色節點搜索完成,移除於openlist,放入closelist。

    對openlist下一個權值最低的節點(原為藍色節點,下圖變為黃色節點)開啟搜索,直線搜索碰到邊界,斜向搜索無果。斜方繼續前進一步,仍然直線搜索碰到邊界,斜向搜索無果。

    由於斜方向還沒結束,繼續前進一步。

    最終在直線方向上發現了紅色節點為跳點,因此藍色節點先被判斷為跳點,只添加藍色節點進openlist。斜方向完成,黃色節點搜索完成。

    最後openlist取出的藍色節點開啟搜索,在水平方向上發現紅色節點,判斷為終點,算法完成。

    回憶起跳點的第三個判斷條件(如果父節點在斜方向,節點x的水平或垂直方向上有滿足條件a,b的點),會發現這個條件判斷是最複雜的。在尋路過程中,它使尋路多次在水平節點上搜到跳點,也只能先添加它本身。其次,這也是算法中需要使用到遞歸的地方,是JPS算法性能瓶頸所在。

    JPS+(Jump Point Search Plus)

    JPS+ 本質上也是 JPS尋路,只是加上了預處理來改進,從而使尋路更加快速。

    預處理

    我們首先對地圖每個節點進行跳點判斷,找出所有主要跳點:

    然後對每個節點進行跳點的直線可達性判斷,並記錄好跳點直線可達性:

    若可達還需記錄號跳點直線距離:

    類似地,我們對每個節點進行跳點斜向距離的記錄:

    剩餘各個方向如果不可到達跳點的數據記為0或負數距離。如果在對應的方向上移動1步后碰到障礙(或邊界)則記為0,如果移動n+1步後會碰到障礙(或邊界)的數據記為負數距離-n

    最後每個節點的8個方向都記錄完畢,我們便完成了JPS+的預處理過程:

    以上預處理過程需要有一個數據結構存儲地圖上每個格子8個方向距離碰撞或跳點的距離。

    示例過程

    做好了地圖的預處理之後,我們就可以使用JPS+算法了。大致思路與JPS算法相同,不過這次有了預處理的數據,我們可以更快的進行直線搜索斜向搜索

    在某個搜索方向上有:

    • 對於正數距離 n(意味着距離跳點 n 格),我們可以直接將n步遠的節點作為跳點添加進openlist
    • 對於0距離(意味着一步都不可移動),我們無需在該方向搜索;
    • 對於負數距離 -n(意味着距離邊界或障礙 n 格),我們直接將n步遠的節點進行一次跳點判斷(有可能滿足跳點的第三條件,不過得益於預處理的數據,這步也可以很快完成)。

    如下圖示,起始節點通過已記錄的向上距離,直接將3步遠的跳點添加進openlist,而不再像以前需要迭代三步(還每步都要判斷是否跳點):

    其它過程也是類似的:

    總結

    可以看到 JPS/JPS+ 算法里只有跳點才會被加入openlist里,排除了大量不必要的點,最後找出來的最短路徑也是由跳點組成。這也是 JPS/JPS+ 高效的主要原因。

    JPS

    • 絕大部分地圖,使用 JPS 算法都會比 A* 算法更快,內存佔用也更小(openlist里節點少了很多)。
    • JPS 在跳點判斷上,要盡可能避免遞歸的深度過大(或者期待一下以後出現避免遞歸的算法),否則在超大型的地圖裡遞歸判斷跳點可能會造成災難。
    • JPS 也可以用於動態變化的地圖,只是每次地圖改變都需要再進行一次 JPS 搜索。
    • JPS 天生擁有合併節點(亦或者說是在一條直線里移除中間不必要節點)的功能,但是仍存在一些可以繼續合併的地方。
    • JPS 只適用於 網格(grid)節點類型,不支持 Navmesh 或者路徑點(Way Point)。

    JPS+

    • JPS+ 相比 JPS 算法又是更快上一個檔次(特別是避免了過多層遞歸判斷跳點),內存佔用則是每個格子需要額外記錄8個方向的距離數據。
    • JPS+ 算法由於包含預處理過程,這讓它面對動態變化的地圖有天生的劣勢(幾乎是不可以接受動態地圖的),因此更適合用於靜態地圖。
    • JPS+ 預處理的複雜度為 \(O(n)\) ,n 代表地圖格子數。
    算法 性能 內存佔用 支持動態地圖 預處理 支持節點類型
    A* 中等 支持 網格、Navmesh、路徑點
    JPS 偏小 支持 網格
    JPS+ 非常快 中等 不支持 有,\(O(n)\) 網格

    綜上,JPS/JPS+ 是A*算法的優秀替代者,絕大部分情況下更快和更小的內存佔用已經足夠誘人。在GDC 2015 關於 JPS+ 算法的演講中,Steve Rabin 給出的數據甚至是比A* 算法快70~350倍。

    參考

    [1] 從頭理解JPS尋路算法 – 簡書 by ElephantKing

    [2] JPS+: Over 100x Faster than A* | GDC 2015

    [3] JPS+ with GoalBounding C++實現,和上面GDC2015的演講人是同一個人 Steve Rabin。

    [4] 一個在線可視化的JPS實現附說明 A Visual Explanation Of Jump Point Search

    [5] JPS 算法原作者論文 github Harabor, Daniel Damir, and Alban Grastien. “Online Graph Pruning for Pathfinding On Grid Maps.” AAAI. 2011.

    博主其它相關文章:
    遊戲AI 系列文章 – KillerAery – 博客園
    遊戲AI之路徑規劃 – KillerAery – 博客園

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

    FB行銷專家,教你從零開始的技巧

  • 02 . Ansible高級用法(運維開發篇)

    02 . Ansible高級用法(運維開發篇)

    自動化任務簡介

    假設我們要在10台linux服務器上安裝一個nginx服務,手動是如何做的?

    # 第一步, ssh登錄NUM(1,n)服務器
    # 第二步,輸入對應服務器密碼
    # 第三步,執行命令: yum install nginx 循環操作n=10
    # 第四步,執行命令: service nginx start
    # 第五步,退出登錄
    

    自動化任務執行的意義

    # 意義一, 提升運維工作效率,減少一份工作成本
    # 意義二, 提高準確度.
    

    自動化任務執行的應用

    # 應用一, 批量命令執行
    # 應用二, 定時程序任務執行
    # 應用三, 批量程序應用服務安裝
    # 應用四, 批量配置文件同步
    # 應用五, 批量代碼部署
    

    ansible配置

    ansible是python中的一套模塊,系統中的一套自動化工具,可以用作系統管理,自動化命令等任務

    ansible優勢
    # 1.ansible是python中的一套完整的自動化執行任務模塊
    # 2.ansible的play_book模式,不用yaml配置,對於自動化任務執行一目瞭然.
    # 3.自動化場景支持豐富
    
    ansible配置文件
    1. inventory
    # 該參數表示資源清單inventory文件的位置,資源清單就是一些Ansible需要連接管理的主機列表  
    # inventory = /root/ansible/hosts
    
    2. library
    # Ansible的操作動作,無論是本地或遠程,都使用一小段代碼來執行,這小段代碼稱為模塊,這個library參數就是指向存放Ansible模塊的目錄  
    # library = /usr/share/ansible
    
    3. forks
    # 設置默認情況下Ansible最多能有多少個進程同時工作,默認設置最多5個進程并行處理。具體需要設置多少個,可以根據控制主機的性能和被管理節點的數量來確定。  
    # forks = 5
    
    4. sudo_user
    # 這是設置默認執行命令的用戶,也可以在playbook中重新設置這個參數
    # sudo_user = root
    # 注意: 新版本已經做了修改,如ansible2.4.1下已經為:
    # default_sudo_user = root
    
    5. remote_port
    # 這是指定連接被關節點的管理端口,默認是22,除非設置了特殊的SSH端口,不然這個參數是不需要被修改的
    # remote_port = 22
    
    6. host_key_checking
    # 這是設置是否檢查ssh主機的秘鑰,可以設置為True或者False
    # host_key_checking = False
    
    7. timeout
    # 這是設置ssh連接的超時間隔,單位是秒
    # timeout = 20
    
    8. log_path
    # ansible系統默認是不記錄日誌的,如果想把ansible系統的輸出記錄到指定地方,需要設置log_path來指定一個存儲Ansible日誌的文件
    
    9. private_key_file
    # 在使用ssh公鑰私鑰登錄系統時使用的秘鑰路徑
    # private_key_file=/path/to/file.pem
    
    ansible.cfg
    [defaults]
    inventory = /tmp/hosts
    forks = 5
    default_sudo_user = root
    remote_port = 22
    host_key_checking = Falsetimeout = 20
    log_path = /var/log/ansible.log
    #private_key_file=/tmp/file.pem
    

    ansible安裝

    # 1. 通過系統的方式,yum,apt,get等
    # 2. 通過python的方式
    
    
    # (推薦)python ./setup.py install 
    easy_install ansible
    pip install ansible	
    

    Ansible基礎操作

    當我們將Ansible安裝好以後,可以通過一些命令開始深入了解Ansible了.
    我們最先展示的並非那強大的集配置,部署,自動化於一身的playbook.而是如何初始化.

    遠程連接概述

    在我們開始前要先理解Ansible如何通過SSH與遠程服務器連接是很重要的.
    Ansible1.3及之後的版本默認會在本地的OpenSSH可用時會嘗試用其遠程通訊,這會啟用ControlPersist(一個性能特性),Kerberos,和在~/.ssh/config中的配置選項如 Jump Host setup.然而,當你使用Linux企業版6作為主控機(紅帽企業版及其衍生版如CentOS),其OpenSSH版本可能過於老舊無法支持ControIPersist,在這些操作系統中,Ansible將會退回並採用paramiko(由Python實現的高質量OpenSSH庫).如果你希望能夠使用像是Kerberized SSH之類的特性,煩請考慮使用Fedora,OS X,或Ubuntu作為你的主控機直到相關平台上有更新版本的OpenSSH可供使用,或者啟用Ansible的”accelerated mode”.

    在Ansible1.2及之前的版本,默認將會使用paramiko,本地OpenSSH必須通過-c ssh或者配置文件中設定.

    我們偶爾會遇到不支持SFTP的設備,雖然很少見,但有概率中獎,可以通過ansible配置文件切換至scp模式來與之連接.

    說起遠程設備,Ansible會默認假定你使用SSH key(當然也推薦這種)但是密碼一樣可以,通過在需要的地方添加-ask-pass選項來啟用密碼驗證,如果使用了sudo特性,當sudo需要密碼時,也同樣適當的提供了-ask-sudo-pass選項.

    也許這是常識,但也值得分享:任何管理系統受益於被管理的機器在主控機附近運行.如果在雲中運行,可以考慮在使用雲中的一台機器來運行Ansible.

    作為一個進階話題,Ansible不止支持SSH來遠程連接.連接方式是插件化的而且還有許多本地化管理的選項諸如管理 chroot, lxc, 和 jail containers.一個叫做‘ansible-pull’的模式能夠反轉主控關係並使遠程系統通過定期從中央git目錄檢出 並 拉取 配置指令來實現背景連接通信

    第一條命令(公鑰認證)

    我們已經安裝ansible了,第一件事就是編輯或者創建/etc/ansible/hosts並在其中加入一個或多個遠程系統,我們的public SSH key必須在這些系統的authorized_keys中.

    # 我們現在ansible控制機上主機名解析
    tail /etc/hosts
    127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
    ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
    121.36.43.223	node1
    120.77.248.31	node2
    116.196.83.113	master
    
    # 將解析過的主機名放到ansible配置文件裏面
    tail -2 /etc/ansible/hosts 
    node1
    node2
    
    # ansible控制機生成公鑰並傳給需要被控制的機器上
    ssh-copy-id node1
    ssh-copy-id node2
    # 因為考慮到安全問題,會有主機秘鑰的檢查,但如果在內網非常信任的服務器就沒必要了.
    sed -i 's/# *StrictHostKeyChecking *ask/StrictHostKeyChecking no/g' /etc/ssh/ssh_config
    
    # 然後我們就可以執行第一條命令來查看能ping通控制的所有節點.
    ansible all -m ping
    node1 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "ping": "pong"
    }
    node2 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "ping": "pong"
    }
    

    Ansible會像SSH那樣試圖用你的當前用戶名來連接你的遠程機器.要覆寫遠程用戶名,只需使用’-u’參數. 如果你想訪問 sudo模式,這裏也有標識(flags)來實現:

    ansible all -m ping -u bruce
    
    ansible all -m ping -u bruce --sudo
    
    ansible all -m ping -u bruce --sudo --sudo-user batman
    

    (如果你碰巧想要使用其他sudo的實現方式,你可以通過修改Ansible的配置文件來實現.也可以通過傳遞標識給sudo(如-H)來設置.) 現在對你的所有節點運行一個命令:

    ansible all -a "/bin/echo hello"
    node1 | CHANGED | rc=0 >>
    hello
    
    node2 | CHANGED | rc=0 >>
    hello
    

    公鑰認證

    Ansible1.2.1及其之後的版本都會默認啟用公鑰認證

    如果有個主機重新安裝並在“known_hosts”中有了不同的key,這會提示一個錯誤信息直到被糾正為止.在使用Ansible時,你可能不想遇到這樣的情況:如果有個主機沒有在“known_hosts”中被初始化將會導致在交互使用Ansible或定時執行Ansible時對key信息的確認提示.

    如果你想禁用此項行為並明白其含義,你能夠通過編輯 /etc/ansible/ansible.cfg or ~/.ansible.cfg來實現:

    [defaults]
    host_key_checking = False
    

    同樣注意在paramiko 模式中 公鑰認證 相當的慢.因此,當使用這項特性時,切換至’SSH’是推薦做法.

    密碼認證

    因為我們接下來要將存取的密碼放到主機清單甚至存到Mysql裏面,我們可以裝一個ssh_pass

    apt-get install sshpass
    

    我們將之前的公鑰.ssh目錄都刪掉,主機名解析不用管.

    注意,刪除.ssh目錄過後記得關閉主機秘鑰檢查.

    tail -3 /etc/hosts
    121.36.43.223	node1
    120.77.248.31	node2
    116.196.83.113	master
    
    tail -2  /etc/ansible/hosts 
    node1
    node2
    
    ansible all -m ping -k
    # 並不是真的ping,只是檢查客戶端的22號端口是否提供工作.不指定用戶默認root用戶
    # -k 輸入密碼
    # -m 指定模塊
    SSH password: 
    node1 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "ping": "pong"
    }
    node2 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "ping": "pong"
    }
    # 如果不想指定用戶名和密碼,避免ansible每次執行到相應主機都會要求輸入密碼.
    tail -2 /etc/ansible/hosts 
    node1 ansible_ssh_user='root' ansible_ssh_pass='youmen'
    node2 ansible_ssh_user='root' ansible_ssh_pass='youmen'  ansible_ssh_port=22
    
    ansible all -m ping
    node1 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "ping": "pong"
    }
    node2 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "ping": "pong"
    }
    

    Ansible常用模塊

    常用模塊

    模塊名 作用 用例
    command 默認模塊 ansible webserver -a “/sbin/reboot” -f 10
    shell 執行shell命令 ansible test -m shell -a “echo $HOSTNAME”
    filetransfer 文件傳輸 ansible test -m copy -a “src=/etc/hosts dest=/tmp/hosts”
    managingpackages 管理軟件包 ansible test -m yum -a “name=nginx state=present”
    user and groups 用戶和組 ansible test -m user -a “name=jeson password=123456”
    Deploying 部署模塊 ansible test -m git -a “repo=https://github.com/iopsgroup/imoocc dest=/opt/iops version=HEAD”
    managingservices 服務管理 ansible test -m service -a “name=nginx state=started”
    BackgroundOperatiions 後台運行 ansible test -B 3600 -a “/usr/bin/running_operation –do-stuff”
    gatheringfacts 搜集系統信息 ansible test -m setup

    playbook

    playbook由YAML語言編寫,YAML參考了其他多種語言,包括: XML,C語言,Python,Perl以及电子郵件格式RFC2822,Clark Evans在2001年5月首次發表了這種語言,另外Ingy dt Net與Oren-Kiki也是這語言的共同設計者.

    playbook的優勢

    # 1. 功能比adhoc更全
    # 2. 控制好依賴
    # 3. 展現更直觀
    # 4. 持久使用
    

    ansible-playbook執行常用命令參數:

    執行方式:ansible-playbook playbook.yml [options]

     -u REMOTE_USER, --user=REMOTE_USER  
    # ssh 連接的用戶名
     -k, --ask-pass    
    # ssh登錄認證密碼
     -s, --sudo           
    # sudo 到root用戶,相當於Linux系統下的sudo命令
     -U SUDO_USER, --sudo-user=SUDO_USER    
    # sudo 到對應的用戶
     -K, --ask-sudo-pass     
    # 用戶的密碼(—sudo時使用)
     -T TIMEOUT, --timeout=TIMEOUT 
    # ssh 連接超時,默認 10 秒
     -C, --check      
    # 指定該參數后,執行 playbook 文件不會真正去執行,而是模擬執行一遍,然後輸出本次執行會對遠程主機造成的修改
    
     -e EXTRA_VARS, --extra-vars=EXTRA_VARS    
    # 設置額外的變量如:key=value 形式 或者 YAML or JSON,以空格分隔變量,或用多個-e
    
     -f FORKS, --forks=FORKS    
    # 進程併發處理,默認 5
     -i INVENTORY, --inventory-file=INVENTORY   
    # 指定 hosts 文件路徑,默認 default=/etc/ansible/hosts
     -l SUBSET, --limit=SUBSET    
    # 指定一個 pattern,對- hosts:匹配到的主機再過濾一次
     --list-hosts  
    # 只打印有哪些主機會執行這個 playbook 文件,不是實際執行該 playbook
     --list-tasks   
    # 列出該 playbook 中會被執行的 task
    
     --private-key=PRIVATE_KEY_FILE   
    # 私鑰路徑
     --step    
    # 同一時間只執行一個 task,每個 task 執行前都會提示確認一遍
     --syntax-check  
    # 只檢測 playbook 文件語法是否有問題,不會執行該 playbook 
     -t TAGS, --tags=TAGS   
    # 當 play 和 task 的 tag 為該參數指定的值時才執行,多個 tag 以逗號分隔
     --skip-tags=SKIP_TAGS   
    # 當 play 和 task 的 tag 不匹配該參數指定的值時,才執行
     -v, --verbose   
    # 輸出更詳細的執行過程信息,-vvv可得到所有執行過程信息。
    
    使用場景

    yaml語法

    1. playbook配置
    ---
    - hosts: 39.108.140.0
      remote_user: root
      vars:
       touch_file: youmen.file
      tasks:
      - name: touch file
        shell: "touch /tmp/{{touch_file}}"
    

    yaml主要由三個部分組成:

    > hosts部分:
    # 使用hosts指示使用哪個主機或主機組來運行下面的tasks,
    # 每個playbook都必須指定hosts,hosts也可以使用通配符格式。
    # 主機或主機組在inventory清單中指定,可以使用系統默認的/etc/ansible/hosts,
    # 也可以自己編輯,在運行的時候加上-i選項,指定清單的位置即可。
    # 在運行清單文件的時候,--list-hosts選項會显示那些主機將會參与執行task的過程中。
    
    > remote_user:指定遠端主機中的哪個用戶來登錄遠端系統,
    # 在遠端系統執行task的用戶,可以任意指定,也可以使用sudo,
    # 但是用戶必須要有執行相應task的權限。
    
    > tasks:指定遠端主機將要執行的一系列動作。tasks的核心為ansible的模塊,
    # 前面已經提到模塊的用法。tasks包含name和要執行的模塊,name是可選的,
    # 只是為了便於用戶閱讀,不過還是建議加上去,模塊是必須的,同時也要給予模塊相應的參數。
    

    執行

    ansible-playbook -i /tmp/hosts --list-hosts ./f1.yaml
    ansible-playbook ./f1.yaml
    
    # 執行結果返回
    # 紅色: 表示有task執行失敗或者提醒的信息
    # 黃色: 表示執行了且改變了遠程主機狀態
    # 綠色: 表示執行成功
    

    yaml語法和數據結構

    yaml語法

    YAML格式是類似於JSON的文件格式,以便於人理解和閱讀,同時便於書寫,首先學習了解一下YAML的格式,對我們後面書寫playbook很有幫助.
    以下為playbook常用到的YAML格式

    # 大小寫敏感
    # 使用縮緊表示層級關係(只能空格不能使用tab)
    # yaml文件以"---"作為文檔的開始
    # 在同一行中,#之後的內容表示註釋,類似於shell,python和ruby.
    # YAML中的列表元素以"-"開頭,然後緊跟着一個空格,後面為元素內容,就像這樣
    - apple
    - orange
    等價於JSON的這種格式
    [
     "apple",
     "orange"
    ]
    
    # 同一個列表中的元素應該保持相同的縮進,否則會被當做錯誤處理.
    # play中hosts,variables,roles,tasks等對象的表示方法都是鍵值中間以":"分割表示,":"後面還要增加一個空格.
    
    變量定義方式

    變量名可以為字母,数字以及下劃線

    playbook里的變量

     1. playbook的yaml文件中定義變量賦值
    > 2. --exxtra-vars執行參數賦給變量
    > 3. 在文件中定義變量
    > 4. 註冊變量
    
    # register關鍵字可以存儲指定命令的輸出結果到一個自定義的變量中.
    ---
    - hosts: database
      remote_user: root
      vars:
        touch_file: youmen_file
      tasks:
        - name: get date
          command: date
          register: date_output
        - name: touch
          shell: "touch /tmp/{{touch_file}}"
        - name: echo  date_output
          shell: "echo {{date_output.stdout}}>/tmp/{{touch_file}}"
    
    數據結構

    yaml支持的數據結構
    字典

    {name:jeson}
    

    列表

    - Apple
    - Mango
    - Orange
    

    純量: 数字,布爾,字符串

    條件判斷
    循環
    循環類型 關鍵字
    標準循環 with_items
    嵌套循環 with_nested
    遍歷字典 with_dict
    并行遍歷列表 with_together
    遍歷列表和索引 with_indexed_items
    遍歷文件列表的內容 with_file
    遍歷目錄文件 with_fileglog
    重試循環 until
    查找第一個匹配文件 with_first_found
    隨機選擇 with_random_choice
    在序列中循環 with_sequence

    條件循環語句復用

    種類一, 標準循環

    ---
    - hosts: nginx
      tasks:
      - name: add serveral users
        user: name={{ item.name }} state=present groups={{ item.groups }}
        with_items:
          - { name: 'testuser1',groups: 'wheel' }
          - { name: 'testuser2',groups: 'root'  }
    

    種類二, 遍歷字典

    ---
    - hosts: nginx
      remote_user: root
      tasks:
        - name: add serveral users
          user: name={{ item.key }} state=present groups={{ item.value }}
          with_dict:
            { 'testuser3':'wheel','testuser4':'root' }
    

    種類三, 遍歷目錄中內容

    ---
    - hosts: nginx
      remote_user: root
      tasks:
       - file: dest=/tmp/aa state=directory
       - copy: src={{ item }} dest=/tmp/bb owner=root mode=600
         with_fileglob:
           - aa/*n
    
    條件語句結合循環語句使用場景
    ---
    - hosts: nginx
      remote_user: root
      tasks:
        - debug: msg="{{ item.key }} is the winner"
          with_dict: {'jeson':{'english':60,'chinese':30},'tom':{'english':20,'chinese':30}}
          when: item.value.english >= 10
    
    異常

    異常處理和相關操作

    異常處理

    1.忽略錯誤

    默認會檢查命令和模塊的返回狀態,遇到錯誤就中斷playbook的執行
    加入參數: ignore_errors: yes
    Example

    - hosts: nginx
      remote_user: root
      tasks:
        - name: ignore false
          command: /bin/false
          ignore_errors: yes
        - name: touch a file
          file: path=/tmp/test22 state=touch mode=0700 owner=root group=root
    

    2. tags標籤處理

    ---
    - hosts: nginx
      remote_user: root
      tasks:
        - name: get process
          shell: touch /tmp/change_test2
          changed_when: false
    

    打標籤

    意義: 通過tags和任務對象進行捆綁,控制部分或者指定的task執行

    # 打標籤
    # 	對一個對象打一個標籤
    # 	對一個對象打多個標籤
    # 	打標籤的對象包括: 單個task任務,include對象,roles對象等.
    

    標籤使用

    -t : 執行指定的tag標籤任務
    --skip-tags: 執行 --skip-tags之外的標籤任務
    
    1. 自定義change狀態
    ---
    - hosts: nginx
      remote_user: root
      tasks:
        - name: get process
          shell: ps -ef |wc -l
          register: process_count
          failed_when: process_count > 3
        - name: touch a file
          file: path=/tmp/test1 state=touch mode=0700 owner=root group=root
    
    roles角色和場景演練

    使用roles角色
    include的用法

    include_tasks/include: 動態的包含tasks任務列表執行
    

    什麼是roles

    是一種利用在大型playbook中的劇本配置模式,在這自己特定結構

    為什麼需要用到roles

    和面向對象開發思想相似
    利用於大型的項目任務中,盡可能的將公共的任務,變量等內容獨立

    劇本結構和設計思路
    ansible官方網站的建議playbook劇本結構如下:

    production        # 正式環境的inventory文件
    staging           #測試環境用得inventory文件
    group_vars/  	  # 機器組的變量文件
          group1        
          group2
    host_vars/   	 #執行機器成員的變量
          hostname1     
          hostname2
    ================================================
    site.yml               # 主要的playbook劇本
    webservers.yml         # webserver類型服務所用的劇本
    dbservers.yml          # 數據庫類型的服務所用的劇本
    
    roles/
          webservers/        #webservers這個角色相關任務和自定義變量
               tasks/
                   main.yml
               handlers/
                   main.yml
               vars/            
                    main.yml
            dbservers/         #dbservers這個角色相關任務和定義變量
                ...
          common/       	  # 公共的
               tasks/        
                    main.yml    
               handlers/    
                    main.yml    # handlers file.
               vars/            # 角色所用到的變量
                    main.yml    # 
    ===============================================
          templates/    #
                ntp.conf.j2 # 模版文件
          files/        #   用於上傳存放文件的目錄
                bar.txt     
                foo.sh     
          meta/         # 角色的依賴
                main.yml   
    
    場景演練

    Nginx工程方式的編譯安裝

    # 劇本分解
    ansible.cfg
      - files		#	存放上傳文件
        - index.html
        - nginx    # 系統init中,控制nginx啟動腳本
        - nginx-1.12.2.tar.gz  # nginx的安裝包文件
    
    production	  # 線上的主機配置文件
    roles		  # roles角色執行
      - apache
      - common
         tasks
           main.yml
         vars
    	main.yml
        meta
        nginx
          - handlers   通過notify觸發
            main.yml
          - tasks
    	  - basic.yml
    	  - main.yml
    	 - nginx.yml
          - vars
    	= main.yml
        tasks
    
    staging			線下測試環境使用的主機配置文件
      - templates		模板(配置,html)
        - nginx1.conf	nginx的自定義conf文件
    webserver.yaml		web服務相關主執行文件
    

    Ansible的核心類介紹

    核心類 用途 所在的模塊路徑
    DataLoader 用於讀取yaml,json格式的文件 ansible.parsing.dataloader
    Play 存儲執行hosts的角色信息 ansible.playbook.play
    TaskQueueManager ansible底層用到的任務隊列 ansible.executor.task_queue_manager
    PlaybookExecutor 核心累執行playbook劇本 ansible.executor.playbook_executor
    CallbackBase 狀態回調,各種成功失敗的狀態 ansible.plugins.callback
    InventoryManager 用於導入inventory文件 ansible.inventory.manager
    VariableManager 用於存儲各類變量信息 ansible.vars.manager
    Host,Group 用於操作單個主機或者主機組信息 ansible.inventory.host
    InventoryManager

    用來管理主機和主機組信息

    from ansible.parsing.dataloader import DataLoader
    from ansible.inventory.manager import InventoryManager
    
    # InventoryManager類
    loader = DataLoader()
    InventoryManager(loader=loader,sources=['youmen_hosts'])
    
    # 1. 添加主機到指定主機組 add_host()  
    # 2. 查看主機組資源get_groups_dict()  
    # 3. 獲取指定的主機對象get_host()
    
    # VariableManager類
    # loader: 實例對象
    # inventory: 調用InventoryManager返回的實例對象.
    VariableManager(loader=loader,inventory=inventory)
    
    # 查看主機變量方法 get_vars()
    # 設置主機變量方法set_host_variable()
    # 添加擴展變量extra_vars
    
    ad-hoc模式調用場景

    ansible -m command -a "ls /tmp" testgroup -i /etc/ansible/hosts -f 5

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※超省錢租車方案

    ※教你寫出一流的銷售文案?

    網頁設計最專業,超強功能平台可客製化

  • 多線程實例——遍歷文件夾分割文件識別文件內容

    多線程實例——遍歷文件夾分割文件識別文件內容

    需求:遍歷文件夾下的所有pdf文件,對每個pdf文件根據二維碼進行分割,再對分割后的文件的內容進行識別。

    可以拆分為以下幾個關鍵方法:

    1.GetFileList方法:遍歷文件,獲取源文件動態數組(這裏假設3個文件夾,每個文件夾下有3個文件,則源文件個數為9),耗時忽略不計

     1 static List<string> GetFileList(string strFilefolder)
     2         {
     3             List<string> list_file = new List<string>();
     4 
     5             for (int i = 0; i <= 2; i++)
     6             {
     7                 for (int j = 0; j <= 2; j++)
     8                     list_file.Add("File" + i + j);
     9             }
    10 
    11             return list_file;
    12         }

    View Code

    2.SplitProcess方法:分割原始pdf文件,識別二維碼(假設耗時500ms),將一個pdf文件分割為N(這裏假設個數為6)個子文件

     1 static void SplitProcess(string sourcefile)
     2         {
     3             Console.WriteLine("SplitFile Start:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));            
     4             for (int i = 0; i <= 5; i++)
     5             {
     6                 //模擬分割單個文件的過程,花費500ms
     7                 Thread.Sleep(500);
     8                 string split_file = sourcefile + i;
     9                 Console.WriteLine("file ready:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    10                 RecognizeProcess(split_file);
    11             }
    12             Console.WriteLine("SplitFile Completed:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));            
    13         }

    View Code

    3.RecognizeProcess方法:識別子文件的內容:加載識別庫,設置識別參數,截取識別區域圖像,圖像處理(如縮放,降噪,灰度轉換等),識別(假設耗時5000ms

    1 static void RecognizeProcess(string split_file)
    2         {
    3             //模擬識別的過程,花費5000ms
    4             Thread.Sleep(5000);
    5             Console.WriteLine("ocrFile Completed:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    6         }

    View Code

    單線程處理:

     1 static void Main(string[] args)
     2         {
     3             Console.WriteLine("Enter Main" + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
     4             string strFilefolder = "";
     5             OcrProcess(strFilefolder);
     6             Console.WriteLine("Main Completed" + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
     7             Console.ReadKey();
     8         }
     9 
    10         static void OcrProcess(string strFilefolder)
    11         {            
    12             List<string> list_sourcefile = GetFileList(strFilefolder);
    13             list_sourcefile.ForEach((sourcefile) =>
    14             {
    15                 Console.WriteLine(sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    16                 //這裏對文件進行分割
    17                 SplitProcess(sourcefile);
    18             });            
    19         }

    View Code

    這個單線程處理的執行結果我們可以預估一下,應該大於 9 * 6 * (0.5 + 5) = 297 秒。

    實際結果:

     …… 

     開始時間 2020-06-17 15:22:28 6104 結束時間 2020-06-17 15:27:26 1541 

    由於是線性處理,整個過程耗費的時間約5分鐘,所以必須要進行優化,所以考慮用多線程來提高效率。

    優化方向:

    1.多線程,使用Task并行對源文件進行分割 

     1 static void OcrProcess(string strFilefolder)
     2         {
     3             List<Task> tasks = new List<Task>();  
     4             List<string> list_sourcefile = GetFileList(strFilefolder);
     5             list_sourcefile.ForEach((sourcefile) =>
     6             {
     7                 Task task = Task.Factory.StartNew( () =>
     8                 { 
     9                     Console.WriteLine(sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    10                     //這裏對文件進行分割
    11                     SplitProcess(sourcefile);
    12                 });
    13                 tasks.Add(task);                
    14             });
    15             Task.WaitAll(tasks.ToArray());
    16         }

    View Code

     ……

     

    開始時間 2020-06-17 15:51:54 5458 結束時間 2020-06-17 15:52:35 3144 

    整個過程耗費的時間約41秒,優化效果明顯。

    2.每分割出來一個文件,開啟子線程,進行識別

     1 static void SplitProcess(string sourcefile)
     2         {
     3             List<Task> tasks = new List<Task>();
     4             Console.WriteLine("SplitFile Start:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));            
     5             for (int i = 0; i <= 5; i++)
     6             {
     7                 //模擬分割單個文件的過程,花費500ms
     8                 Thread.Sleep(500);
     9                 string split_file = sourcefile + i;
    10                 Console.WriteLine("file ready:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    11                 Task task = Task.Factory.StartNew(() =>
    12                 {
    13                     RecognizeProcess(split_file);
    14                 });
    15                 tasks.Add(task);
    16             }
    17             Task.WaitAll(tasks.ToArray());
    18             Console.WriteLine("SplitFile Completed:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));            
    19         }

    View Code

      ……

     開始時間 2020-06-17 15:58:59 2591 結束時間 2020-06-17 15:59:28 9051 

    整個過程耗費的時間約29秒,運行時間進一步縮短。 

    然而,最後再思考一下,如果把多線程發揮到極致,理想狀態應該是多少秒執行完畢?

    以源文件sourcefile=File00為例,

    第一個分割子文件split_file=File000,ReadyTime 500ms,Ocr Completed Time 應該為5500ms

    第二個分割子文件split_file=File001,ReadyTime 1000ms,Ocr Completed Time 應該為6000ms 

    第三個分割子文件split_file=File002,ReadyTime 1500ms,Ocr Completed Time 應該為6500ms 

    第四個分割子文件split_file=File003,ReadyTime 2000ms,Ocr Completed Time 應該為7000ms

    第五個分割子文件split_file=File004,ReadyTime 2500ms,Ocr Completed Time 應該為7500ms

    第六個分割子文件split_file=File005,ReadyTime 3000ms,Ocr Completed Time 應該為8000ms

    File00 Split Completed!

    每一個源文件(sourcefile)被逐個分割為6個拆分子文件(split_file)並識別完成,都需要8000ms時間,如果使用線程同步的話,那麼後續源文件也同步被分割並識別完成。 

    所以,理想情況下,應該是8秒,而與29秒差距太大了,應該還有優化空間!

    3.怎麼優化?向什麼方向優化?我們不妨不用Task,回歸到Thread本身來試試。

    可是Thread運行時沒有Task.WaitAll()這樣的控制方法,因此,我們還要引入WaitHandle和ManualResetEvent來進行多線程管理。 

     1 class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             Console.WriteLine("Enter Main" + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
     6             string strFilefolder = "";
     7             OcrProcess(strFilefolder);
     8             Console.WriteLine("Main Completed" + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
     9             Console.ReadKey();
    10         }        
    11 
    12         static void OcrProcess(string strFilefolder)
    13         {            
    14             List<ManualResetEvent> split_waits = new List<ManualResetEvent>();
    15             List<string> list_sourcefile = GetFileList(strFilefolder);
    16             list_sourcefile.ForEach((sourcefile) =>
    17             {
    18                 Thread m_thread = new Thread(() =>
    19                 {
    20                     ManualResetEvent mre = new ManualResetEvent(false);
    21                     split_waits.Add(mre);
    22                     Console.WriteLine(sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    23                     //這裏對文件進行分割
    24                     SplitProcess(sourcefile);
    25                     mre.Set();
    26                 });
    27                 m_thread.Start();
    28             });
    29             WaitHandle.WaitAll(split_waits.ToArray());
    30         }
    31 
    32         static void SplitProcess(string sourcefile)
    33         {
    34             Console.WriteLine("SplitFile Start:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    35             var ocr_waits = new List<EventWaitHandle>();
    36             for (int i = 0; i <= 5; i++)
    37             {
    38                 //模擬分割單個文件的過程,花費500ms
    39                 Thread.Sleep(500);
    40                 string split_file = sourcefile + i;
    41                 Console.WriteLine("file ready:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    42                 ManualResetEvent mre_child = new ManualResetEvent(false);
    43                 ocr_waits.Add(mre_child);
    44                 Thread m_child_thread = new Thread(() =>
    45                 {
    46                     Console.WriteLine("m_child_thread enter:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    47                     RecognizeProcess(split_file);                    
    48                     mre_child.Set();
    49                     Console.WriteLine("m_child_thread after set:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    50                 });
    51                 m_child_thread.Start();
    52             }
    53             WaitHandle.WaitAll(ocr_waits.ToArray());
    54             Console.WriteLine("SplitFile Completed:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));            
    55         }
    56 
    57         static void RecognizeProcess(string split_file)
    58         {
    59             //模擬識別的過程,花費5000ms
    60             Thread.Sleep(5000);
    61             Console.WriteLine("ocrFile Completed:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
    62         }
    63 
    64         static List<string> GetFileList(string strFilefolder)
    65         {
    66             List<string> list_file = new List<string>();
    67             for (int i = 0; i <= 2; i++)
    68             {
    69                 for (int j = 0; j <= 2; j++)
    70                     list_file.Add("File" + i + j);
    71             }
    72             return list_file;
    73         }
    74     }

    View Code

      

      ……

    開始時間 2020-06-17 15:28:17 2397 結束時間 2020-06-17 16:28:27 9151 

    整個過程耗費的時間約10秒,運行時間與理論的8秒已經十分接近(因為Thread創建以及運行時需要切換上下文,Console.WriteLine都有一定的耗時,PC性能好的話應該更接近8秒),可以說目標已經達成。 

    Tips:

    ManualResetEvent初始狀態為false表示不將線程信號量初始值置為signal,線程會自動往下執行,執行Set()方法時,將線程信號量置為signal。

    WaitHandle.WaitAll(split_waithandle1,split_waithandle2); //一直等待,直到split_waithandle1,split_waithandle2信號量均被置為signal才會往下執行。

    不足之處:

    開啟Thread要受到系統的限制,所以本例線程數必須考慮操作系統線程最大值限制。

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※教你寫出一流的銷售文案?

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※回頭車貨運收費標準

    ※別再煩惱如何寫文案,掌握八大原則!

    ※超省錢租車方案

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!