分類: 未分類

透過Azure DataStudio / SSMS查詢效能不佳的SQL語法

透過Azure DataStudio / SSMS查詢效能不佳的SQL語法

這兩天遇到一個情境,一個早上突然所有的請求都卡住,平均回應 時間都要1~2分鐘。當下一時間沒啥方向,往登入那邊查,結果找錯了方向,會這樣想純粹是因為認為那個是一條測試的必經之路。而我們還在開發的資料庫中,最多也才幾百筆,最大 也不到一萬筆,為什麼會突然 這麼慢呢?

後來請 DBA看了一下後,馬上就找到問題了,大至記錄如下:

情境:
假如現在有兩張表
分別為
表 1 Master(Id)
表 2 Detail(Id, MasterId)

當時Master表大概有9xx筆,而Detail表有7千筆左右

select * from Master m
left join Detail d on m.Id = d.MasterId
where m.xxxxxx = xxxxxx

結果這樣不得了,一查發現大概執行個5、6百次 ,總執行時間就超過4小時以上了..
一般來說,PrimaryKey需設是Unique+Index(Cluster)因此只以Id作搜尋條件,效能不會差
但此時,Foreign Key若子表的Column沒有建Index,則對搜尋效能仍無影響,FK只作為 限制功能

補充以上觀點後

再補上SqlServer這次如何查找有問題的效能語法的方法(我是Mac,突然很羨慕ssms完整版功能)
不過我也立馬補上Azure Data Studio的方法,不無小補 一下。

1.Azure Portal對有問題的database進行 “查詢效能深入解析” (Azure Portal上的DB查詢權限要開放)

ex: 透過下列清單,可以查詢到Top5慢的查詢,透過查詢ID,可以再往 展開

註 : 這個畫面出不來的話代表 Azure Portal上對DB的權限不夠,不過至少可以對到查詢 ID,再請DBA或用 SSMS/Azure Data Studio去細追

2.透過SSMS,需要DBA針對db user進行開放

Query Store(查詢存放區)
-> Top Resource Consuming
-> Tracked Queries
-> QueryId

SSMS功能就是強大 ,可以針對 時間/CPU/IO統計

對於我們的查詢 ,在一個時間區間內,Where 條件不一樣 ,可能就QueryId就會不同(Plan Id就會不一樣)
-> ViewQueryTest查詢其問題語法分析 執行計劃

透過 查詢計劃可以看到發生了兩次Scan

3.透過Azure Data Studio,加入dashboard圖表

最後一種,若你是用Mac的環境(M1 Core),沒有辦法安裝SSMS這套工具的話

教學:https://learn.microsoft.com/en-us/azure-data-studio/tutorial-qds-sql-server

設定完重啟,在Database-> Manage就可以看到以下的圖表,也可以對齊上述方法的查詢Id

能看到非常 明顯 的峰點,那就還算是好的情境~(特別是還在 測試區)

針對該圖表 可以往下 看Insight的清單,可以對到1525就是問題查詢的所在

從query_sql_text就可以拿到問題語法,一樣貼到Azure Data Studio的Query窗

就可以Estimated Plan來顯示查詢計劃囉

DateTimeOffset在ToString()的神秘事件

DateTimeOffset在ToString()的神秘事件

最近早上08:xx多的時候,跑本機整測突然一個測試跑不過 。

下來Debug查說是比對值驗證 錯誤

測試情境是 datetimeoffset的value會存進資料庫,再查出來直接 做比對是否 正確

突然就這麼錯了覺得很怪,往下 追查,其比對的值出現了
2024/1/1 12:00:00 +00:00 vs 2024/1/1 00:00:00 +00:00

的情況,想說應該是24小時制的問題,因此寫了一小段程式驗證

        var valueHour12 = new DateTimeOffset(2024, 1, 1, 12, 0, 0, 0, offset: TimeSpan.FromHours(0));
        var displayToHour12 = new DateTimeOffset(2024, 1, 1, 0, 0, 0, 0, offset: TimeSpan.FromHours(0));
        
        var s = valueHour12.ToString("o");
        var sWithO = valueHour12.ToString();
        
        Console.WriteLine("valueHour12");
        Console.WriteLine(s);
        Console.WriteLine(sWithO);
        
        Console.WriteLine("displayToHour12");
        s = displayToHour12.ToString("o");
        sWithO = displayToHour12.ToString();
		
        object boxingHour12Am = displayToHour12;
        
        Console.WriteLine(displayToHour12.Hour);
        Console.WriteLine(s);
        Console.WriteLine(sWithO);
        Console.WriteLine(boxingHour12Am);
        //tostring 失真點
        var parsedDTOffset = DateTimeOffset.Parse(boxingHour12Am.ToString());
        Console.WriteLine(parsedDTOffset.Hour);
        
        //明確格式 不會失真
        parsedDTOffset = DateTimeOffset.ParseExact(displayToHour12.ToString("yyyy/MM/dd hh:mm:ss t z"), "yyyy/MM/dd hh:mm:ss t z", new CultureInfo("zh-TW"));
        Console.WriteLine(parsedDTOffset.Hour);

輸出結果如下,會發現指定12點的 跟指定0點的,在ToString()後,再解回Datetimeoffset的結果,hour會飄掉,具體來看是他現顯示成如下

試著把Console印出來結果如下

valueHour12
2024-01-01T12:00:00.0000000+00:00
2024/1/1 12:00:00 +00:00
displayToHour12
0
2024-01-01T00:00:00.0000000+00:00
2024/1/1 12:00:00 +00:00
2024/1/1 12:00:00 +00:00
12 //不明確格式解成了 12的數字
0 //明確格式可以正確解成0的Hour

這個知道為什麼後,解法很簡單,就是明確轉字串格式

不過 為什麼說神秘?

因為首先Windows的OS上不會有這個情況,ToString()至少會有PM, AM或上午, 下午,所以這個案例在Windows的PC上跑測試,目前看起來都 測不出來。目前是我是在Mac的Rider上測試,或許會有OS 的環境、語言差異,造成這個影響,不過我是從Rider上直接看到他的變數可視化 就是直接忽略了上午/下午,這個IDE上差異就比較特別,在Windows的Rider跟VS則都有上午/下午。IDE與OS之餘,可能也可以在Windows上安裝 Docker跑Linux看看,不過 若ToString()可以重現 0點變 成12的Hour屬性 ,就代表 非Windows系統就會有這種ToString()失真的情況!

第二個,只要過了 12點這個臨界點,例如台灣時間9點,代表 UTC凌晨1:00,再ToString(),就 不會有這個問題,可以解出 1的Hour,但是若是指定13點,就又會出現這個情況,一樣會解出1,分別會變成1點PM跟 13點,這樣的測試時間點若加上台灣時間的shifting(-8),很容易混淆,或是誤以為沒問題,且若固定 時間,或隨機時間導致測試測不出來,而不知哪天會有重大的狀況。

設定為 13的情境如下:

例如我們都設定 是 測試資料為 早上10點,那這個問題不會炸,不然就是我們設定 固定 CICD整測 跑的時間都是早上11點,那可能抓到的時間也都沒問題

因此需要小心程式中任何 地方 datetimeoffset,tostring(), 序列化 或 檔案化 的部分會不會有失真的情節 ,將其格式明確統一地定好。

而今天 因為9:00前上班被抓到而探討一下這個問題也實屬運氣成份吧?

在Azure AppService Docker Alpine Container上連線MSSQL遇到 網路不通的問題

在Azure AppService Docker Alpine Container上連線MSSQL遇到 網路不通的問題

最近使用AppService佈署Container服務

結果在連線資料庫的時候 一直掛掉

A network-related or instance-specific error occurred while establishing a connection to SQL Server.

Container的Linux版本是Alpine

因此安裝套件的指令是改用用apk add,不是apt-get

以後telnet套件無法直接安裝,而是被封裝進 busybox-extras 

Ref: https://github.com/gliderlabs/docker-alpine/issues/397

apk add busybox-extras

但實測Telnet下

app# busybox-extras telnet sea-dev-xxxx-sqlsrv.database.windows.net 1433

是通的,但程式還是連不過去

後來終於在這篇找到了一點蛛絲馬跡

https://github.com/dotnet/dotnet-docker/issues/2346

馬上試了一下
apk add icu-libs

果然通了,浪費了我二個小時XD

LeetCode 20大挑戰- 清單-置頂

LeetCode 20大挑戰- 清單-置頂

新增HackerRank筆記

  • New Year Chaos(Link)

Finished

  1. LRU Cache (Done) Link
    • List, dictionary, linked list
  2. Median of Two Sorted Arrays Link
    • Array merge, sorting
    • 困難的是如何達成O(log(m+n))
  3. Palindrome Number Link
    • string function
  4. Majority Element Link
  5. H-Index Link
  6. Two-Sum Link
    • HashTable
  7. Task Scheduler Link
  8. Three-Sum Link
    • Two Pointer
  9. Reverse Interger Link

Selected

  • Container With Most Water
    • Array, Two Pointer, Greedy
  • Reverse Nodes in k-Group(Hard) – Problem
    • Linked List, Recursion
  • Regular Expression Matching – Problem
    • String, Dynamic Programming, Recursion
  • Longest Substring Without Repeating Characters – Problem
    • String, hashtable , Sliding window
  • Substring with Concatenation of All Words
    • Sliding Window , string, HashTable
  • Minimum Path Sum
    • Array, DP, Matrix
  • Best Time to Buy and Sell Stock III (Best Time to Buy and Sell Stock IV)
    • Array, DP

LeetCode Median of Two Sorted Arrays

LeetCode Median of Two Sorted Arrays

看到Hard就來看看

Given two sorted arrays nums1 and nums2 of size m and n respectively, return the median of the two sorted arrays.

The overall run time complexity should be O(log (m+n)).

Example 1:

Input: nums1 = [1,3], nums2 = [2]
Output: 2.00000
Explanation: merged array = [1,2,3] and median is 2.

Example 2:

Input: nums1 = [1,2], nums2 = [3,4]
Output: 2.50000
Explanation: merged array = [1,2,3,4] and median is (2 + 3) / 2 = 2.5.

Constraints:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -106 <= nums1[i], nums2[i] <= 106
直覺寫法
public class Solution {
    public double FindMedianSortedArrays(int[] nums1, int[] nums2) {
                var merged = nums1.Concat(nums2);

        var mergedArray = merged.OrderBy(a => a).ToArray();
        
        if (mergedArray.Length % 2 > 0)
        {
            var median = mergedArray[(mergedArray.Length - 1) / 2];
            return median;
        }
        else
        {
            var medianFirst = mergedArray[((mergedArray.Length) / 2)-1];
            var medianLast = mergedArray[((mergedArray.Length) / 2)];
            return ((double)medianFirst + (double)medianLast) / 2;
        }
    }
}

結果就這樣過了,到底為什麼是Hard?

看了某些人也是這樣寫
Complexity:
Time complexity: The time complexity is dominated by the sorting operation, which takes O(N log N) time, where N is the total number of elements in nums1 and nums2.
Space complexity: The space complexity is determined by the size of the combined list nums, which is O(N), where N is the total number of elements in nums1 and nums2.

public class Solution {
    public double FindMedianSortedArrays(int[] nums1, int[] nums2) {
         List<int> nums = nums1.ToList();
            nums.AddRange(nums2.ToList());
            nums.Sort();
            if(nums.Count % 2 == 1)
                return nums[nums.Count/2];

            return (nums[nums.Count/2] + nums[nums.Count/2 - 1])/2.0;
    }
}

所以這題的難度感覺是如何 說明其時間複雜度吧XD

終於為部落格加上SSL設定(劇情急轉直下)

終於為部落格加上SSL設定(劇情急轉直下)

我的網站在http的世界中許久 ,這陣子終於想來弄SSL環境了(因為 老婆要寫網址XD) 先前我記得是卡在PH …

以上就是庫存頁檔救回的一小段字 XD

無意見發現多年前設定的mariadb,密碼一點都沒有安全性,結果就被破解了

苦腦許久,發現我在 6月初有備份image,至少還救的回2022年以前的文章 Orz

目前我先 救到這邊…

後續故事跟被入侵綁架的故事再補上….

以下是phpmyadmin被瘋逛try 密碼的logs(從docker logs來的)

資料庫還給我留下警語

.NetCore的ApiGateway +Consul的本機實驗

.NetCore的ApiGateway +Consul的本機實驗

一直以來都知道.netcore可以開始跨平台運作在linux上,因此運作在 docker的container中也是可行的

不過最近因為要研究apigateway,需要模擬infra的架構 ,因此需要在本機環境buildup起來多台主機的模式

沒想到入坑玩了一下,就發現,其實眉眉角角也不少,但來來回回操作後,其實是相對會愈來愈熟悉

首頁,我solution的構成包含了以下幾層 (apigateway還可做loadbalance,再加個nginx就可以, 不是這次的重點)

1.Redis(多台 instance作counter的cache)
2.WebApp(.net core)
3.ApiGateway(.net core + ocelot.net sdk)
4.Consul(service discovery service 作ha)

這次主要研究的機制為 3跟4之間的協作機制
基本上,有了3,預設走configuration就可以手動配置 ,但若要能做到動態ha的話,就必須加入service discovery的機制,剛好ocelot框架有支援 consul(還有其他套,但預設consul)

docker-compose

為了方便 重建,這次採用 docker-compose的yaml來維護(以下情境的container -name 都一般化了,只需要區分角色就好)

version: "3"

services:
  nginx:
    build: ./nginx
    container_name: core-app.local
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - core-app-gateway1
    # network_mode: "host"
    networks:
      - backend

  consul1:
    image: consul
    container_name: node1
    command: agent -server -bootstrap-expect=2 -node=node1 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1
    # network_mode: "host"
    networks:
      - backend

  consul2:
    image: consul
    container_name: node2
    command: agent -server -retry-join=node1 -node=node2 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1
    depends_on:
      - consul1
    # network_mode: "host"
    networks:
      - backend

  consul3:
    image: consul
    hostname: discover-center
    container_name: node3
    command: agent -retry-join=node1 -node=node3 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1 -ui
    ports:
      - 8500:8500
    depends_on:
      - consul1
      - consul2
    #network_mode: "host"
    networks:
      - backend

  redis:
    container_name: myredis
    image: redis:6.0.6
    restart: always
    ports:
      - 6379:6379
    privileged: true
    command: redis-server /etc/redis/redis.conf --appendonly yes
    volumes:
      - $PWD/data:/data
      - $PWD/conf/redis.conf:/etc/redis/redis.conf
    networks:
      - backend

  core-app-discover-a1:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp1
    environment:
      - HOSTNAME=http://0.0.0.0:40001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-a1
      - CLIENT_PORT=40001
      - SERVICE_NAME=core-app-discover-a
      - RedisConnection=redis:6379,password=12345678,allowAdmin=true
    depends_on:
      - redis
      - consul3
    ports:
      - "40001"
    networks:
      - backend

  core-app-discover-a2:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp2
    environment:
      - HOSTNAME=http://0.0.0.0:40001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-a2
      - CLIENT_PORT=40001
      - SERVICE_NAME=core-app-discover-a
      - RedisConnection=redis:6379,password=12345678,allowAdmin=true
    depends_on:
      - redis
      - consul3
    ports:
      - "40001"
    networks:
      - backend

  core-app-discover-a3:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp3
    environment:
      - HOSTNAME=http://0.0.0.0:40001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-a3
      - CLIENT_PORT=40001
      - SERVICE_NAME=core-app-discover-a
      - RedisConnection=redis:6379,password=12345678,allowAdmin=true
    depends_on:
      - redis
      - consul3
    ports:
      - "40001"
    networks:
      - backend

  core-app-discover-b:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp-b
    environment:
      - HOSTNAME=http://0.0.0.0:50001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-b
      - CLIENT_PORT=50001
      - SERVICE_NAME=core-app-discover-b
      - RedisConnection=192.168.1.115:6379,password=xxxxxxx
    depends_on:
      - redis
      - consul3
    ports:
      - "50001:50001"
    networks:
      - backend

  core-app-discover-c:
    build: ./dotnetapp/dotnetapp-discover
    hostname: webapp-c
    environment:
      - HOSTNAME=http://0.0.0.0:50002
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://core-app-discover-c
      - CLIENT_PORT=50002
      - SERVICE_NAME=core-app-discover-c
      - RedisConnection=192.168.1.115:6379,password=xxxxxxx
    depends_on:
      - redis
      - consul3
    ports:
      - "50002:50002"
    networks:
      - backend

  core-app-gateway1:
    build: ./dotnetapp/dotnetapp-gateway
    hostname: gateway1
    environment:
      - HOSTNAME=http://0.0.0.0:20001
      - SERVER_HOSTNAME=http://192.168.1.115
      - SERVER_PORT=8500
      - CLIENT_HOSTNAME=http://192.168.1.115
      - CLIENT_PORT=20001
      - SERVICE_NAME=core-app-gateway1
    depends_on:
      - consul3
    ports:
      - 20001:20001
    networks:
      - backend

networks:
  backend:
    driver: bridge

以上yaml就會建立一個network讓 container之前可以用 hostname來通訊。

這次研究的過程,會發現一件事情就是若要綁service discovery,那麼每個末端的webapp都需要跟register center做註冊,並提供一個health接口。但是若要做到auto scale,那依賴docker-compose所做的scale就會綁定動態的host_name(有序號),因此在此不建議 用scale直接長n台容器,而是先用多個端點來建置(如我的a1、a2、a3三台)
這樣就可以指定 hostname跟port給 webapp,而不會被scaling的機制而無法抓到動態的port跟名稱

.netcore application的Dockerfile

以下是webapp的dockerfile (gateway的打包方式差不多,就不貼了)

FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
WORKDIR /src
COPY ["dotnetapp-discover.csproj", "dotnetapp-discover/"]
RUN dotnet restore "dotnetapp-discover/dotnetapp-discover.csproj"
COPY . ./dotnetapp-discover
WORKDIR "/src/dotnetapp-discover"
RUN dotnet build "dotnetapp-discover.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "dotnetapp-discover.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "dotnetapp-discover.dll"]

為了計數器功能與多台sharing cache,因此這次 還 模擬了一下連redis
而redis的connection string 可以透過 environment variable 注入進來(docker-compose可以另外用.env的檔案來管理不同環境,有興趣可以看 官方文件),而在.netcore的c#程式中,要抓到docker的參數的話可以使用GetEnvironmentVariable來取得設定值

    public static IServiceCollection AddCacheService(this IServiceCollection services)
    {
        var redisConnection = Environment.GetEnvironmentVariable("RedisConnection");
#if DEBUG
        redisConnection = "192.168.1.115:6379,password=xxxxxx,allowAdmin=true";
#endif
        
        services.AddSingleton<IConnectionMultiplexer>(
            ConnectionMultiplexer.Connect(redisConnection)
        );
        return services;
    }

註∶為了反覆測試,才加上allow admin,不然這個開關是很危險的,可以直接flush掉你的所有快取(清空)

Redis記錄的Log型態

註冊Service Discovery

ApiGateway跟WebApp我都有跟Consul註冊Service Discovery
因此這次主要的寫法也是follow consul client sdk的套件,Consul的註冊管理上,同一個Name的會被Grouping起來,ID要給不一樣的,若ServiceName都不一樣,就算是一樣的程式,還是會被視為不同節點,這樣會無法作HA的判斷

static void RegisterConsul(string serviceName, string serverHostName , string serverPort, string clientHostName , string clientPort, IHostApplicationLifetime appLifeTime)
{
    using var consulClient = new Consul.ConsulClient(p => 
        { p.Address = new Uri($"{serverHostName}:{serverPort}"); });
    var registration = new AgentServiceRegistration()
    {
        Check = new AgentServiceCheck()
        {
            Interval = TimeSpan.FromSeconds(10),
            HTTP = $"{clientHostName}{(clientPort == string.Empty ? "" : $":{clientPort}")}/health",
            Timeout = TimeSpan.FromSeconds(5),
            Method = "GET",
            Name = "check",
        },
        ID = Guid.NewGuid().ToString(),
        Name = serviceName,
        Address = Dns.GetHostName(),
        Port = int.Parse(clientPort),
        
    };

    try
    {
        consulClient.Agent.ServiceRegister(registration).Wait();
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }

    //web服務停止時,向Consul解註冊
    consulClient.Agent.ServiceDeregister(registration.ID).Wait();
}

以上function可以在program.cs的startup時期先進行註冊,而這個function所有的參數一樣可以來自於 docker-compose
範例∶

自動生成HealthCheck端點

在webapp的startup可以使用以下語法就長出health接口

builder.Services.AddHealthChecks();

app.MapHealthChecks("/health",  new HealthCheckOptions()
{
    ResultStatusCodes =
    {
        [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Healthy] = StatusCodes.Status200OK,
        [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Degraded] = StatusCodes.Status200OK,
        [Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
});

但還是要視服務加上MapHealthChecks的ResultStatus的設定,以consul來說,就是回200就好了,若沒有加上這個設定,其實health的回應並不是200,所以會被視為失敗

正常情況打 health的接口

Ocelot的設定檔

最後就是ocelot的json設定,傳統的Ocelot自已就有Load-Balance機制,因此若有多台Downstream的服務,本來就可以用以下設定去管理服務

不過這樣做的缺點就是若隨時要增減 容器/主機時,都要再去重新配置設定檔。造成主機可能的reset與維運上的難度,因此透過Service Discovery的好處就浮上了臺面!以下的配置,只需要注意,不用定義DownStream的Host,只要給一個ServiceName,這個ServiceName要跟WebApp在Startup的時候註冊的一樣。並在最後GloablConfiguration有註冊Service DiscoveryProvider的設定,就可以打通ApiGateway跟Service Discovery之間的便利機制。

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/test",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/newapi/get",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a",
      "LoadBalancerOptions": {
        "Type": "LeastConnection"
      }
    },
    {
      "DownstreamPathTemplate": "/test/list",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/newapi/list",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a",
      "LoadBalancerOptions": {
        "Type": "LeastConnection"
      }
    },
    {
      "DownstreamPathTemplate": "/test",
      "DownstreamScheme": "http",
      "DownstreamHttpMethod": "DELETE",
      "UpstreamPathTemplate": "/newapi/reset",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a"
    }, 
    {
      "Key": "api1",
      "DownstreamPathTemplate": "/test/list",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/newapi/list1",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a"
    },
    {
      "Key": "api2",
      "DownstreamPathTemplate": "/test/list",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/newapi/list2",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "ServiceName": "core-app-discover-a"
    }
  ],
  "Aggregates": [
    {
      "RouteKeys": [
        "api1",
        "api2"
      ],
      "UpstreamPathTemplate": "/newapi/listall"
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://0.0.0.0:20001",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "192.168.1.115",
      "Port": 8500,
      "Type": "PollConsul",
      "PollingInterval": 100
    },
    "QoSOptions": {
      "ExceptionsAllowedBeforeBreaking": 0,
      "DurationOfBreak": 0,
      "TimeoutValue": 0
    },
    "LoadBalancerOptions": {
      "Type": "LeastConnection",
      "Key": null,
      "Expiry": 0
    },
    "DownstreamScheme": "http",
    "HttpHandlerOptions": {
      "AllowAutoRedirect": false,
      "UseCookieContainer": false,
      "UseTracing": false
    }
  }
}

上述的ApiGateway一樣用DockerFile把專案包成Image

整合測試

docker-compose up -d –force-recreate

會看到一次起來這麼多服務

http://localhost:20001/newapi/get(透過api gateway打到downstream的service)

這邊多請求幾次會發現machineName一直在webapp1~3跳來跑去,採用load-balance的機制,ocelot會採用你設定的策略進行請求分派

ApiGateway有Aggregation的功能,就是可以把多個Api請求Combine在一起,讓請求變成1次,減少頻寬的消耗,這邊我也做了配置,來試看看吧

出於Ocelot.json的設定檔段落:

透過api gateway的組合讓 多個 Api可以一次 回傳

最後,打了很多次請求後,來總覽一下所有的請求摘要(程式是自已另外撈Redis的Request記錄來統計)

http://localhost:20001/newapi/list(透過api gateway打到down stream的service)

嘗試Docker Stop某一台 Discover-a1的服務,Consul DashBoard馬上就看的到紅點了!

這個時候再去打 之前load-balance的端點http://localhost:20001/newapi/get,會發現machineName會避開stop掉的服務只剩下 webapp1跟webapp3在輪循,因此做到了後端服務品質的保護

小插曲

ID不小心用成了Guid.New,所以會導致重新啟動就又拿到新的ID,造成了多一個Instance。但舊的也活了,因為他們的HealthCheck是一樣的

其實ApiGateway的議題,從Infra的角度上,可以玩的東西還有很多。

像是他也支援RateLimiting, QualityOfService(Polly) , 服務聚合(Aggregation)等機制,都可以從他們的官方文件看到設定方式,算是配置上不算太複雜。

不過 Ocelot在.net這邊發展一陣子後,近一年好像停更了!取而代之的好像是其他語言寫的更強的Apigateway: Envoy。

Envoy資源
https://www.envoyproxy.io/docs/envoy/latest/intro/what_is_envoy
https://www.netfos.com.tw/Projects/netfos/pages/solution/plan/20210111_plan.html

若只是要輕量級的,或是想參考其架構設計的,倒是可以翻github出來看看~

Github∶ https://github.com/ThreeMammals/Ocelot

這次Lab程式碼

若要拿去玩的,請把Ip, Password自行調整一下

一些文章的參考

https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html?highlight=ServiceDiscoveryProvider

https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html?highlight=ServiceDiscoveryProvider#

https://www.uj5u.com/net/232249.html

.NET Core WebApi 加入CORS (.net 7.0 up)與allow預檢要求

.NET Core WebApi 加入CORS (.net 7.0 up)與allow預檢要求

有時候我們希望測試Client端呼叫Server的Api,但若domain不相同或是沒有合理的開放的話,Browser會直接進行安全性的封鎖

<html>
    <head>
    </head>
    <body>
        <script language="javascript">
            window.onload = function(e){
                // Make a request for a user with a given ID
                axios.get('https://localhost:5001/api/test/history')
                .then(function (response) {
                    // handle success
                    console.log(response.status);
                })
                .catch(function (error) {
                    console.log(error.response.status);
                    // handle error
                    console.log(error);
                })
                .then(function () {
                    // always executed
                });
            }
        </script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js" integrity="sha512-odNmoc1XJy5x1TMVMdC7EMs3IVdItLPlCeL5vSUPN2llYKMJ2eByTTAIiiuqLg+GdNr9hF6z81p27DArRFKT7A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    </body>
</html>

若我們真的去打了一個404不存在的網頁,但實際上Server有接到請求,所以回的了404,但是js端卻會因為CORS的Policy導致報錯,這個錯誤的例外,會導致js拿到ERR_NETWORK而且也無其他訊息或完整的異常資訊

.Net Core Startup.cs中,找到以下段落
//public void ConfigureServices(IServiceCollection services)內
services.AddCors(options => { options. AddPolicy(name: "MyAllowSpecificOrigins", policy => { policy. AllowAnyOrigin() . AllowAnyHeader(); }); });
//public void Configure(IApplicationBuilder app, IWebHostEnvironment env)內

app.UseRouting();
            
app.UseCors("MyAllowSpecificOrigins");

app.UseSentryTracing(); // 要啟用 Sentry 上的 Performance Monitoring 需加這行

app.UseAuthentication();

備註:UseCors的呼叫必須放在UseRouting之後 ,但在UseAuthorization之前,若有具名的policy,就也要具名UseCors加進來,若有具名但沒有UseCors(“xxxxxx”)的話,仍視為無效

所以成功加進去後,axios套件也是可以抓到

2024/01/30更新

大部分的CORS設定若是在Azure的app service仍可以透過CORS的Portal設定去處理掉

不過若是在本機的環境,有遇到下面的錯誤訊息(host有改過所以不是localhost)

Access to XMLHttpRequest at 'https://dev.azurewebsites.net:5001/api/Auth/app-login' from origin 'https://dev.azurewebsites.net:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

代表被preflight預檢請求擋掉了

這個時候在Api的Program.cs

var app = builder.Build(); 
//之後加上
app.UseCors(options =>
    options
        .WithOrigins("https://dev.azurewebsites.net:3000", "*") 
         //note Allow Any Origin或直接"*"不會過 
        .AllowAnyHeader()
        .AllowAnyMethod()
        .AllowCredentials());
//之前加上
app.UseHttpsRedirection();

MSDN Reference∶

https://learn.microsoft.com/zh-tw/aspnet/core/security/cors?view=aspnetcore-8.0

SQL Azure 垂直Partitioned DB Query T-SQL範例

SQL Azure 垂直Partitioned DB Query T-SQL範例

SQL Azure的連線字串雖然允許你在SQL工具上切換資料庫,但是其實要下Cross Database的T-SQL是沒有辦法允許的
因此我們需要利用Elastic database query + External Data Source來協助我們

以下假設我們有主資料庫 (A),與延伸資料庫(B)

在B的資料庫中
我們先建立以下的CREDENTIAL資訊

CREATE MASTER KEY ENCRYPTION BY PASSWORD = '{A資料庫的密碼}';
CREATE DATABASE SCOPED CREDENTIAL ds_pool WITH IDENTITY = '{A資料庫的帳號}',SECRET = '{A資料庫的密碼}';
--以下語法提供方便重新建立/測試
--drop MASTER KEY --drop DATABASE SCOPED CREDENTIAL ds_pool

ds_pool不需要用字串包起來,你可以命名成你可理解的名稱,下面我們會再用到
執行成功的話,接著執行 以下建立Remote DataSource的物件

CREATE EXTERNAL DATA SOURCE REMOTE_DS WITH ( 
   TYPE=RDBMS, 
   LOCATION='{A資料庫的Localtion}stage.database.windows.net', 
   DATABASE_NAME='{A資料庫的名稱}', 
   CREDENTIAL= ds_pool );

--以下語法提供方便重新建立/測試
--Drop EXTERNAL DATA SOURCE REMOTE_DS

這邊我們要給定一個REMOTEDS到DataSouce,並指定他的CREDENTIAL與資料庫名稱、類型與 Location,從MSDN上可以看到Location似乎可以支援檔案格式,可以填寫blob的位置,這個有機會再研究

CREATE EXTERNAL TABLE [dbo].[{A資料庫裡面的Table Or View都可以}] 
 ( [A資料庫的某一個table的欄位1] [欄位型別1], 
   [A資料庫的某一個table的欄位2] [欄位型別2], 
   .....略
 ) WITH ( DATA_SOURCE = REMOTE_DS )

--drop EXTERNAL TABLE [dbo].[{A資料庫裡面的Table Or View都可以}]
select * from [dbo].[{A資料庫裡面的Table Or View都可以}]

這邊我們必須在B資料庫端,定義 A資料庫希望 remote存取的物件的schema,可以完整也可以只撈取部分~~端看需求,在B資料庫端,也可以給定一個新的別名,語法可以參考以下。

CREATE EXTERNAL TABLE [dbo].[remote_xxxxxx]
( [A資料庫的某一個table的欄位1] [欄位型別1], 
   [A資料庫的某一個table的欄位2] [欄位型別2], 
   .....略
 ) WITH ( 
    DATA_SOURCE = REMOTE_DS
    ,SCHEMA_NAME = 'A資料庫裡面的Table Or View的schema,通常是dbo'
    ,OBJECT_NAME = '{A資料庫裡面的Table Or View}'
)

實測後,確實看起來是可以運作查詢,透過在B資料庫的連接中,直接查詢A

這與Elastic Pool似乎不一樣,而目前我們使用 到的費用也是包含在SQL Database中因此不另外計費

Currently, the elastic database query feature is included into the cost of your Azure SQL Database.

註1∶但是開一台elastic pool的主機,至少2000nt/月起跳…用途與解決問題的目的性應該不太一樣吧(沈思…)

註2∶若你的store procedure中有使用到External DataSource,語法中有使用With (nolock)的話,會出現 – “Table hints are not supported on queries that reference external tables”.的錯誤訊息

Reference

https://docs.microsoft.com/en-us/azure/azure-sql/database/elastic-query-getting-started-vertical

https://social.technet.microsoft.com/wiki/contents/articles/37170.azure-sql-database-using-external-data-sources.aspx