用1/10到成本为节点运营者启用零认证下载

将Cloudflare的R2服务作为Sui网络快照的存储库大大降低了提供这些800GB文件的成本。

用1/10到成本为节点运营者启用零认证下载

在Sui网络上运行的验证节点和完整节点需要具有最高水平的可靠性和运行时间,以便提供高吞吐量及区块链的可扩展性。可靠地运行有状态应用的关键部分,确保可以相对轻松地进行硬件故障转移。如果磁盘故障或其他类型的故障影响到运行验证节点的机器,应该有一种简单的方法来迁移验证节点,而无需重新处理所有链历史。 

确保无缝故障转移的地方就是快照。Sui网络中的状态快照有两种形式,分别是Formal和Database。Formal快照包含了在epoch结束时构成验证节点共识信息的最小状态。Database快照实际上是节点数据库的完全副本。 

快照如果不存储在可以轻松和可靠地访问的地方,就不会有太多效用。在Sui网络创世开始上传快照时,亚马逊网络服务(AWS)的S3是可靠的后端存储的理想选择,可以与Sui网络上的早期节点运营者共享。Mysten Labs开始在S3上托管公共快照,任何节点运营者都可以使用这些快照快速同步完整节点或验证节点,并将其引入网络。 

然而,托管允许公共下载状态快照的S3存储桶的简单行为实际上比我们预期的更加痛苦。状态快照的格式要求您使用AWS命令行界面(CLI)来下载单个快照中包含的许多文件。如果在调用AWS CLI时没有预先存在的AWS凭据可供插入,您需要使用AWS CLI的轻微文档化的指令: aws s3 cp --no-sign-request

除了用户问题之外,Sui的快照正在呈指数增长。作为一个超高吞吐量的区块链,Sui生成的数据量几乎是空前的。每次下载状态快照时,都需要从S3拉取800多GB的数据到节点运营者的主机上,这需要几个小时的时间。

对于熟悉AWS S3定价背后数学的任何读者来说,公共资源 s3://mysten-mainnet-snapshots 的成本很快就会变得昂贵。S3会按每传输出S3的数据每GB收费。由于大多数运营商在AWS之外运行节点,因此这适用于Mysten Lab的快照存储桶。我们很快就会产生每月五位数的费用来托管这一公共资源。

为验证节点和完整节点提供状态快照导致每天从AWS S3出口的数据量接近40TB。

Cloudflare最近宣布了R2,这是S3的竞争对手,其定价模型非常独特:零出口费用。这非常适合托管定期获取的数据集,并且在S3上具有巨大的优势特点。 

与其进行完全迁移不同,我们选择将R2作为S3的替代来源,并将S3迁移到请求者付费模型。S3具有出色的性能和全球传输加速等功能,我们不想放弃这些功能。这次迁移的主要部分不是调整我们的工具来写入R2(R2与S3 API兼容),而是修改Sui应用程序以轻松从R2读取。 

支持R2上的无权限下载

要求用户使用AWS CLI对R2进行操作并不是一个可以接受的体验;相反,我们希望用户能够将我们的工具指向 db-snapshot.mainnet.sui.io ,并在那里无需任何认证即可读取托管的文件(对我们来说,具有零认证选项很重要,因为我们希望尽可能让任何人都能运行Sui完整节点)。 

AWS S3请求签名是与Amazon S3资源安全交互的关键方面。当对S3存储桶进行请求时,无论是上传对象、下载文件、列出对象还是其他任何操作,都需要对请求进行签名,以确保其真实性和完整性。该过程通常涉及使用用户的访问密钥(访问密钥ID、秘密访问密钥)对请求进行签名,从而创建经过身份验证的HTTP请求。 

对于公开访问的文件或对象,技术上可以绕过对大多数云提供商的请求签名以进行读取(但不能进行列出)资源的操作,但这在我们使用的Rust对象存储库中不受支持。因此,我们决定在我们的代码库中添加此支持。我们希望添加零认证支持,但让用户可以选择在启用请求者付费模式的情况下通过签名请求从S3还原快照,或者从R2进行无签名读取。为了干净地完成这个过程,我们首先在我们的代码库中声明了常见对象存储操作的抽象化。

#[async_trait]
pub trait ObjectStoreGetExt: std::fmt::Display + Send + Sync + 'static {
   /// Return the bytes at given path in object store
   async fn get_bytes(&self, src: &Path) -> Result<Bytes>;
}

#[async_trait]
pub trait ObjectStoreListExt: Send + Sync + 'static {
   /// List the objects at the given path in object store
   async fn list_objects(
       &self,
       src: Option<&Path>,
   ) -> object_store::Result<BoxStream<'_, object_store::Result<ObjectMeta>>>;
}

#[async_trait]
pub trait ObjectStorePutExt: Send + Sync + 'static {
   /// Write the bytes at the given location in object store
   async fn put_bytes(&self, src: &Path, bytes: Bytes) -> Result<()>;
}

#[async_trait]
pub trait ObjectStoreDeleteExt: Send + Sync + 'static {
   /// Delete the object at the given location in object store
   async fn delete_object(&self, src: &Path) -> Result<()>;
}

为了在签名和无签名实现之间进行清晰切换,我们的使用函数也需要做一些修改:

/// Read object at the given path from input store using either signed or 
/// unsigned store implementation
pub async fn get<S: ObjectStoreGetExt>(store: &S, src: &Path) -> Result<Bytes>;

/// Returns true if object exists at the given path
pub async fn exists<S: ObjectStoreGetExt>(store: &S, src: &Path) -> bool;

/// Write object at the given path. There is no unsigned put implmenetation
/// because writing an object requires permissioned user signing requests
pub async fn put<S: ObjectStorePutExt>(store: &S, src: &Path, bytes: Bytes) -> Result<()>;

然后,我们实现了上述特征的签名实现(通过回退到我们已经使用的对象存储库)和无签名实现(利用各个云提供商的REST API): 

/// Implementation for making signed requests using object store lib
#[async_trait]
impl ObjectStoreGetExt for Arc<DynObjectStore> {
   async fn get_bytes(&self, src: &Path) -> Result<Bytes> {
       self.get(src)
           .await?
           .bytes()
           .await
           .map_err(|e| anyhow!("Failed to get file: {} with error: {}", src, e.to_string()))
   }
}

/// Implementation for making unsigned requests to [Amazon 
/// S3](https://aws.amazon.com/s3/).
#[derive(Debug)]
pub struct AmazonS3 {
   /// Http client wrapper which makes unsigned requests for S3 resources
   client: Arc<S3Client>,
}

#[async_trait]
impl ObjectStoreGetExt for AmazonS3 {
   async fn get_bytes(&self, location: &Path) -> Result<Bytes> {
       let result = self.client.get(location).await?;
       let bytes = result.bytes().await?;
       Ok(bytes)
   }
}

/// Implementation for making unsigned requests to [Google Cloud 
/// Storage](https://cloud.google.com/storage/).
#[derive(Debug)]
pub struct GoogleCloudStorage {
   /// Http client wrapper which makes unsigned requests for gcs resources
   client: Arc<GoogleCloudStorageClient>,
}

#[async_trait]
impl ObjectStoreGetExt for GoogleCloudStorage {
   async fn get_bytes(&self, location: &Path) -> Result<Bytes> {
       let result = self.client.get(location).await?;
       let bytes = result.bytes().await?;
       Ok(bytes)
   }
}
pub struct ObjectStoreConfig {
  /// Which object store to use i.e. S3, GCS, etc
  #[serde(skip_serializing_if = "Option::is_none")]
  #[arg(value_enum)]
  pub object_store: Option<ObjectStoreType>,
  /// Name of the bucket to use for the object store. Must also set
  /// `--object-store` to a cloud object storage to have any effect.
  #[serde(skip_serializing_if = "Option::is_none")]
  #[arg(long)]
  pub bucket: Option<String>,
  #[serde(default)]
  #[arg(long, default_value_t = false)]
  pub no_sign_request: bool,
  ...
}

impl ObjectStoreConfig {
  pub fn make_signed(&self) -> Result<Arc<DynObjectStore>, anyhow::Error> {
    match &self.object_store {
      Some(ObjectStoreType::File) => self.new_local_fs(),
      Some(ObjectStoreType::S3) => self.new_s3(),
      Some(ObjectStoreType::GCS) => self.new_gcs(),
      _ => Err(anyhow!("At least one backed is needed")),
    }
  }
}

pub trait ObjectStoreConfigExt {
   fn make_unsigned(&self) -> Result<Arc<dyn ObjectStoreGetExt>>;
}

impl ObjectStoreConfigExt for ObjectStoreConfig {
   fn make_unsigned(&self) -> Result<Arc<dyn ObjectStoreGetExt>> {
       match self.object_store {
           Some(ObjectStoreType::S3) => {
               let bucket_endpoint = { };
               Ok(AmazonS3::new(&bucket_endpoint).map(Arc::new)?)
           }
           Some(ObjectStoreType::GCS) => {
               let bucket_endpoint = { };
               Ok(GoogleCloudStorage::new(&bucket_endpoint)).map(Arc::new)?)
           }
           _ => Err(anyhow!("At least one backend is needed")),
       }
   }
}

有了以上所有内容,我们可以根据用户提供的配置清晰地在签名和无签名实现之间切换:

let store: Arc<dyn ObjectStoreGetExt> = if store_config.no_sign_request {
  store_config.make_unsigned()?
} else {
  store_config.make_signed().map(Arc::new)?
}; 

我们接近实现零认证快照下载的目标,但还没有到达。最后一个挑战涉及无法在R2存储桶中列出文件而无需签名请求(在S3上可以允许公开的、无签名的列出访问)。在下载RocksDB快照目录之前,我们需要列出其中的文件。我们通过在快照创建过程中添加一个包含所有文件路径的MANIFEST文件来解决了这个问题。现在,这个MANIFEST文件是快照目录中所有文件及其相对路径的真实来源。

最终结果

最终,通过将R2作为默认的快照下载选项,我们能够将提供这些快照的成本降低约70至80%,同时降低了Sui网络中节点的启动和故障转移的障碍。

注意:本内容仅供一般教育和信息目的,不应被解释或依赖为对任何资产、投资或金融产品的购买、销售或持有的认可或建议,也不构成财务、法律或税务建议。