WEBSITE ĐANG PHÁT TRIỂN

Episerver Commerce trên Azure - những gì không có trong tài liệu chính thức

Tài liệu Episerver/Optimizely rất đầy đủ cho môi trường on-premise, nhưng khi lên Azure thì có ít nhất 5 vấn đề sẽ đốt của bạn mà không ai nói trước. Tôi đã trải qua đủ cả, và đây là những gì tôi rút ra.

Cái ngày tôi nghĩ mình hiểu Episerver

Năm đó là lần đầu tiên tôi deploy Episerver Commerce lên Azure App Service. Team tôi đã đọc hết documentation, làm theo từng bước trong deployment guide. Local chạy ngon. Staging chạy ngon. Production deploy xong - thở phào.

Rồi 3 tiếng sau, Lucene index bắt đầu corrupt.

Khách hàng gọi điện: "Tìm kiếm sản phẩm không ra gì hết." Lúc đó là 11 giờ đêm. Tôi ngồi debug đến 3 giờ sáng mới hiểu vấn đề.

Đó là lần đầu trong nhiều lần tôi học được rằng: Episerver trên Azure không giống Episerver trên server vật lý - dù tài liệu không nói thẳng điều đó.


Vấn đề 1: Lucene index và shared file system

Episerver Find mặc định dùng Lucene.NET lưu index trên local file system. Trên môi trường single server, không vấn đề gì. Nhưng khi scale out trên Azure App Service với 2+ instances - mỗi instance có file system riêng, không shared.

Kết quả: Instance A index xong, Instance B vẫn đọc index cũ. Hoặc tệ hơn, 2 instances ghi đồng thời vào cùng một path trên Azure File Share → corrupt.

Fix đúng cách:

// Trong Startup.cs hoặc Program.cs
services.Configure<FindOptions>(options =>
{
    // Tuyệt đối không dùng local path trên multi-instance Azure
    // options.DefaultIndex = "local-index"; // ← ĐỪNG LÀM NÀY

    // Dùng Episerver Find Service (hosted) thay vì local Lucene
    options.ServiceUrl = Environment.GetEnvironmentVariable("EPISERVER_FIND_SERVICE_URL");
    options.DefaultIndex = Environment.GetEnvironmentVariable("EPISERVER_FIND_INDEX_NAME");
});

Bài học: Nếu bạn dùng Episerver Find hosted service (không phải local Lucene), vấn đề này không xảy ra. Nhưng nếu bạn đang dùng local Lucene để tiết kiệm chi phí - hãy chuyển sang hosted, hoặc chắc chắn rằng bạn chỉ chạy 1 instance duy nhất (và có plan khi cần scale).


Vấn đề 2: Schema migration trong Episerver Commerce

Episerver Commerce có cơ chế tự động chạy database migration khi khởi động. Trên on-premise, điều này ổn vì chỉ có một process khởi động tại một thời điểm.

Trên Azure, khi bạn deploy và có deployment slots hoặc multiple instances khởi động đồng thời - hai instances có thể cùng chạy migration một lúc.

Tôi đã thấy cảnh này: Instance 1 đang chạy migration, Instance 2 cũng bắt đầu migration, cả hai cùng ALTER TABLE trên SQL Azure → deadlock → cả hai crash → site down hoàn toàn.

Cách xử lý:

// Tạo startup lock với Azure Blob Storage
public class MigrationStartupTask : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        var services = context.Services;
        services.AddSingleton<IStartupLockProvider, AzureBlobStartupLockProvider>();
    }

    public void Initialize(InitializationEngine context)
    {
        var lockProvider = context.Locate.Advanced.GetInstance<IStartupLockProvider>();

        using (var lockHandle = lockProvider.AcquireLock("episerver-migration", TimeSpan.FromMinutes(5)))
        {
            if (lockHandle != null)
            {
                // Chỉ một instance được chạy migration
                RunMigrations(context);
            }
        }
    }
}

Hoặc đơn giản hơn: dùng deployment slot "swap" thay vì deploy trực tiếp. Swap đảm bảo instance cũ vẫn chạy đến khi instance mới healthy hoàn toàn - và chỉ 1 instance khởi động tại một thời điểm.


Vấn đề 3: Connection pool trên Azure SQL

Azure SQL có giới hạn connection tùy theo tier. Episerver Commerce mở khá nhiều connection - catalog service, order service, scheduled jobs, và nếu bạn có Episerver Find thì thêm vài connection nữa.

Trên Standard S2 (50 DTU), tôi từng thấy "connection pool exhausted" vào giờ cao điểm mà không hiểu tại sao. Code không thay đổi, traffic bình thường - nhưng bỗng dưng site chậm hẳn và bắt đầu timeout.

Nguyên nhân thực sự: Episerver có các background job chạy định kỳ (scheduled jobs). Mặc định chúng không pool connection đúng cách nếu bạn không cấu hình Max Pool Size explicitly.

<!-- connectionStrings trong web.config hoặc appsettings.json -->
<add name="EPiServerDB"
     connectionString="Server=xxx.database.windows.net;Database=mydb;
                       User Id=xxx;Password=xxx;
                       Max Pool Size=30;
                       Min Pool Size=5;
                       Connection Timeout=30;
                       MultipleActiveResultSets=True;" />

Với Azure SQL, tôi recommend Max Pool Size không quá 30 trên Standard tier, và upgrade lên Premium nếu bạn cần nhiều hơn. Đừng để mặc định (100) - Azure SQL sẽ throttle bạn trước khi pool đầy và bạn sẽ không hiểu tại sao.


Vấn đề 4: Custom tables và Episerver migration

Episerver Commerce có hệ thống migration riêng. Khi bạn thêm custom tables cho order extension hoặc catalog extension, Episerver cần biết về chúng.

Vấn đề xảy ra khi team tôi có một junior developer thêm custom table trực tiếp vào database mà không đăng ký với Episerver migration framework. Môi trường dev ổn vì database đã có table đó. Nhưng khi deploy lên staging mới - boom, exception ngay từ startup vì table không tồn tại.

Pattern đúng:

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Commerce.Initialization.CommerceInitialization))]
public class CustomTableMigration : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        var databaseHandler = context.Locate.Advanced
            .GetInstance<IDatabaseHandler>();

        // Kiểm tra và tạo table nếu chưa có
        if (!databaseHandler.TableExists("CustomOrderMetadata"))
        {
            databaseHandler.Execute(@"
                CREATE TABLE CustomOrderMetadata (
                    OrderId INT NOT NULL,
                    MetaKey NVARCHAR(100) NOT NULL,
                    MetaValue NVARCHAR(MAX),
                    CreatedDate DATETIME2 DEFAULT GETUTCDATE(),
                    CONSTRAINT PK_CustomOrderMetadata PRIMARY KEY (OrderId, MetaKey)
                )
            ");
        }
    }

    public void Uninitialize(InitializationEngine context) { }
}

Quy tắc tôi đặt ra cho team: Không được tạo table trong database bằng tay. Mọi schema change phải đi qua IInitializableModule. Ai vi phạm thì tự xử lý production incident đêm đó :D


Vấn đề 5: App Service tier và scheduled jobs

Episerver Commerce có nhiều scheduled jobs quan trọng: index rebuild, inventory update, price import, v.v. Những job này cần chạy liên tục và ổn định.

Vấn đề: Trên Azure App Service, "Always On" phải được bật - nhưng chỉ có từ Basic tier trở lên. Free và Shared tier tắt app sau 20 phút idle. Kết quả là scheduled jobs của bạn không chạy, inventory stale, search index không được cập nhật.

Và đây là điều tài liệu không nói rõ: Ngay cả với "Always On" bật, App Service vẫn có thể recycle process sau 29 tiếng (mặc định). Nếu scheduled job của bạn chạy hơn 29 tiếng (ví dụ: full catalog re-index cho 500k SKU), job sẽ bị kill giữa chừng.

Cấu hình:

// appsettings.json
{
  "EPiServer": {
    "ScheduledJob": {
      "Enabled": true,
      "MaximumExecutionTime": "24:00:00"
    }
  }
}

Và trong Azure Portal: App Service → Configuration → General Settings → App Service managed pipeline = Integrated, Platform = 64 Bit, Always On = On.

Với catalog import lớn, tôi recommend chuyển sang Azure WebJob hoặc Azure Functions với durable functions thay vì Episerver scheduled job - linh hoạt hơn nhiều và không bị giới hạn bởi app lifecycle.


Kinh nghiệm từ dự án thực

Dự án lớn nhất tôi deploy Episerver Commerce lên Azure là một nền tảng e-commerce multi-tenant, phục vụ gần 20 store khác nhau, mỗi store có catalog riêng. Tổng cộng khoảng 300k SKU.

Những vấn đề tôi liệt kê ở trên - tôi đã gặp đủ cả, không thiếu cái nào. Mỗi cái là một production incident và một đêm thức muộn. Nhưng sau khi fix hết, hệ thống đó chạy ổn định hơn 3 năm không có major incident.

Bài học lớn nhất: Episerver là một framework xuất sắc, nhưng nó được thiết kế cho môi trường on-premise trước. Cloud deployment là later-stage addition. Khi bạn đưa nó lên Azure, bạn cần hiểu rõ cả hai bên - Episerver internals và Azure constraints - mới có thể làm đúng.


Gửi các bạn trẻ

Nếu bạn đang bắt đầu với Episerver/Optimizely Commerce:

  1. Đọc documentation - nhưng đừng tin tuyệt đối. Docs viết cho môi trường lý tưởng. Production thì không lý tưởng.
  2. Test trên môi trường giống production nhất có thể - cùng tier Azure, cùng số instances, cùng scheduled jobs chạy.
  3. Luôn có rollback plan trước mỗi deployment. Episerver database migration không tự rollback.
  4. Monitoring là bắt buộc - Application Insights tích hợp sẵn với Azure, không có lý do gì để không dùng.

Bạn đã gặp vấn đề nào chưa?

Nếu bạn đang làm việc với Episerver/Optimizely trên Azure, tôi rất muốn nghe kinh nghiệm của bạn - đặc biệt nếu bạn có vấn đề mà tôi chưa đề cập ở đây. Để lại comment nhé 👇


/Son Do - believe in basic

#1percentbetter #Episerver #Optimizely #Azure #dotnet #ecommerce


Bài viết liên quan

Xem thêm
E-commerce & Search Systems

Caching strategy cho product catalog - invalidation mới là bài toán khó

Cache thì dễ thêm vào. Nhưng khi product price thay đổi lúc 11:59 PM trước flash sale lúc 12:00 AM, bạn mới biết cache invalidation khó như thế nào. Bài này đi sâu vào các pattern thực tế và code C# cho product catalog. Phil Karlton có câu nói nổi tiếng: "There are only two hard things in Computer Science: cache invalidation and naming things." Tôi thêm vào: cache invalidation trong e-commerce còn khó hơn cache invalidation ở chỗ khác. Vì trong e-commerce, data thay đổi liên tục - price, stock, promotion - và mỗi inconsistency đều có thể cost bạn money (hoặc cost khách hàng).

E-commerce & Search Systems

Azure Event Hub trong pipeline xử lý đơn hàng real-time

Azure Event Hub không phải message queue - đây là streaming platform. Sự khác biệt này ảnh hưởng đến mọi design decision. Bài này là architecture và code cho order processing pipeline dùng Event Hub - từ kinh nghiệm thực tế.

E-commerce & Search Systems

Search relevance: tại sao người dùng tìm 'áo đỏ' lại ra 'váy xanh

Search relevance không phải là "tìm từ nào match từ đó". Đằng sau một kết quả tìm kiếm là cả một hệ thống scoring phức tạp - và nếu không hiểu nó, bạn sẽ cứ nhận complaint "search dở" mà không biết fix ở đâu.