3

我的 Heroku 平台 ASP.NET Core + PostgreSQL 筆記

 2 years ago
source link: https://blog.darkthread.net/blog/aspnetcore-heroku-postgresql/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
我的 Heroku 平台 ASP.NET Core + PostgreSQL 筆記-黑暗執行緒

這幾天寫 LINE 機器人,新認識一個好用的 Heroku 平台。

整合 LINE API時,我們必須要提供一個 HTTPS 網址(術語叫 Webhook)供 LINE API 呼叫,開發期間可在本機跑 ASP.NET Core 再靠 ngrok 串接一個公開 ℎttps://隨機名稱.ngrok.io 網址,將呼叫導到本機,甚至能用 Visual Studio Line By Line 偵錯。但實際上線時,就得找到地方把 ASP.NET Core 程式部署上去執行,而且得裝 TLS 憑證跑 HTTPS。Heroku 平台提供一套很簡便的上線程序,用 Email 就能申請一個 512M RAM/500MB 磁碟空間的免費程式執行環境(類似 Docker 的輕量級 Linux 容器,Heroku 術語叫 Dyno),程式寫好要部署很簡單,git push 將程式碼推上 Heroku,它便會用預先設定好的建置程序編譯並部署上線。如果想在 Heroku 上跑 ASP.NET Core,蔡煥麟老師的這篇部署 ASP.NET Core 2.1 應用程式到 Heroku 平台有詳細介紹,照著做應該都能順順完成。

如果用 Heroku 跑測試或消遣性質的程式,Heroku 的免費方案雖然有些限制,對簡單應用已經夠用。免費方案的限制包括:

  1. 帳號註冊後為未驗證(Unverfied)狀態,提供有效信用卡號可升級為已驗證(Verified)狀態,已驗證帳號可以享用更多資源。
  2. 每個帳號可建立 5 個 App,驗證後可增加到 100 個
  3. 每個 App 最多可以跑一支 Web Dyno/一支 Worker Dyno/一支 One-Off Dyno(類似排程)
  4. Dyno 執行時間有上限,每個月有 550 Dyno 小時(一個 Dyno 跑一小時)的額度,驗證後可增加到 1000 小時
  5. 每個 Dyno 可用記憶體為 512MB
  6. App 閒置 30 分鐘會自動休眠以節省 Dyno 小時
  7. 驗證帳號可自訂 Domain Name,取代 ℎttps://app-name.herokuapp.com 網域名稱

在本機用 ngrok 測試沒問題,把 LINE 機器人雛型丟上 Heroku,也順利跑完測試,但我很快發現一個大問題。

Heroku 免費方案的 App 預設閒置 30 分鐘就會自動休眠以節省 Dyno 小時,下次有人存取程式會再自動啟動,理論上只會稍微延遲無傷大雅。我原本也是這樣想的,但因為我是用 ASP.NET Core SQLite 資料庫,知道每次重新部署會資料會消失,打算寫個匯出機制必要時匯出資料備份,但後來發現事情跟我想的不一樣。

Dyno 採用暫存式案系統 Ephemeral Filesystem,程式執行期間寫入的檔案在 Dyno 關機或重啟後就會清除,每次啟動時會恢復到剛部署的樣子,就跟 Docker 的概念一樣(在 Docker 也要靠 --volume 對映實體檔案)。但 Heroku 所謂的 Sleep 後再恢復就等同重啟,故一不小心 30 分鐘沒用資料就消失了。關於此類需求,Heroku 的建議是改用 AWS S3 服務保存靜態檔案或用 Postgres 資料庫保存資料。

Heroku 有 PostgreSQL 免費方案,有 10,000 筆資料跟最多 20 條連線的限制,但簡單應用取代 SQLite 綽綽有餘。所以我們又有機會體驗 EF Core 的優勢,改幾行程式將 SQLite 換成 Postgres!

用 Heroku 工具為 App 加掛 PostgreSQL heroku addons:create heroku-postgresql:hobby-dev,專案從 NuGet 安裝 PostgresSQL EF Core 程式庫 dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL,改 Startup.cs 換 Provider (延伸閱讀:EF Core 筆記 4 - 跨資料庫能力展示),輕鬆秒殺... 才怪,魔鬼在細節裡。第一次用 PostgreSQL,新手有很多坑要踩,另外我的 EF Core 多資料庫經驗不足,觀念不夠清楚,一開始有點走錯路,下次應該會順利多了。

下面整理我這次學到的經驗:

  1. Heroku 的 PostgreSQL 連線字串會存在 DATABASE_URL,但它是 postgres://userId:[email protected]:5432/xxxxx 格式,需要做些轉換變成 PostgreSQL 用的連線字串。而在歷經一段無法連線錯誤後:
    Npgsql.NpgsqlException (0x80004005): no pg_hba.conf entry for host "xxx.xxx.xxx.xxx", user "xxxxxxxx", database "xxxxxxxx", SSL off
    Npgsql.NpgsqlException (0x80004005): Exception while performing SSL handshake, The remote certificate was rejected by the provided RemoteCertificateValidationCallback.
    
    我學到連線字串要多指定 SslMode = true、TrustServerCertificate = true(之前研究 SQL 加密連線的知識派上用場),才能符合 Heroku 環境的要求。DATABASE_URL 轉為連線字串的函式範例如下:
    string GetPostgreSqlCnstr()
    {
        //https://stackoverflow.com/a/53292619/288936
        var databaseUrl = Environment.GetEnvironmentVariable("DATABASE_URL");
        if (string.IsNullOrEmpty(databaseUrl)) return null;
        var databaseUri = new Uri(databaseUrl);
        var userInfo = databaseUri.UserInfo.Split(':');
    
        var builder = new NpgsqlConnectionStringBuilder
        {
            Host = databaseUri.Host,
            Port = databaseUri.Port,
            Username = userInfo[0],
            Password = userInfo[1],
            Database = databaseUri.LocalPath.TrimStart('/'),
            SslMode = SslMode.Require,
            TrustServerCertificate = true
        };
        return builder.ToString();
    }
    
  2. 我應用 ASP.NET Core CRUD 傻瓜範例 (2) - 資料庫準備文章裡「加入自動建立或升級資料表」技巧,我加了一段程式,偵錯到 DATABASE_URL 變數時 AddDbContext<PollDbContext>(options => options.UseNpgsql(...))、否則 AddDbContext<PollDbContext>(options => options.UseSqlite(...)) 其他程式未改的情況下,如意算盤是同一套程式會依環境,在本機或 Docker 中用 SQLite,在 Heroku 自動改用 PostgreSQL。部署到 Heroku 後,程式順利在 PostgreSQL 建立資料表並正常啟動,但高興沒多久,後來陸續出現奇怪問題。例如:Entity 有個 bool Enabled { get; set; } 在 SQLite 執行好好的,但換成 PostgreSQL 後出現 42804: column "Enabled" is of type integer but expression is of type boolean,將 Enabled 改成 int 用 0/1 表示 false/true 繞過問題,後來另一段 LineOpinions.Where(o => o.GroupId == groupId && o.Time >= beginTime && o.Time <= endTime) 則是冒出 Npgsql.PostgresException (0x80004005): 42883: operator does not exist: text >= timestamp without time zone,我開始覺得不太對。
    這時我才想到,不同的資料庫對映的欄位型別不同,換不同資料庫要重新執行 ef core migrations add InitialCreate 欄位才會正確對映:

    以 DateTime 為例,SQLite 用 TEXT,PostgreSQL 是用 timestamp without time zone。EF Core 多資料提供者共用 DbContext 的標準做法,是另建獨立 Migration Project,為求省事,我先用 #define 加 #if 做條件化編譯繞過,改為 PostgreSQL 建立 Migration 物件,並刪掉資料庫,終於正常了。
  3. Heroku Dyno 預設的時區是 UTC+0,設定 App 變數 TZ = Asia/Taipei
  4. 以下的程式寫法,在 SQLite 沒問題,但在 PostgreSQL 會抱怨已經有另一個 Command 在執行 Npgsql.NpgsqlOperationInProgressException (0x80004005): A command is already in progress: SELECT l."GroupId", l."Enabled", l."GroupName", l."UnlockKey"
     foreach (var g in DbCtx.LineGroups)
     {
         var grpId = g.GroupId;
         var userNames = DbCtx.LineGroupMembers.Where(o => o.GroupId == grpId)...
         //...略...
     }
    
    改成 foreach (var g in DbCtx.LineGroups.ToArray()) 後避開問題。
  5. 使用指令 heroku logs 可以檢視 ASP.NET Core 執行時輸出的 Log 查問題,預設只會顯示最後 100 行,初期錯誤遍地開花時可加上 -n 10000 放大到一萬行。另外 --tail 可以即時監看 Log,測試偵錯很方便。

最後用這張 Git Log 軌跡紀念我的 2021 中秋黑客松,第一次開著 ASP.NET Core 挺進 Heroku + PostgreSQL! (一個 Commit 代表一個坑,哈!)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK